* feat(fusion): let judge use its own knowledge and override the panel (#6804) The judge prompt said to write an answer 'grounded in that analysis', implicitly capping output at the panel's union. When all panel members miss or are collectively wrong on something, the judge should apply its own reasoning as a full participant and override consensus, while keeping an honesty guard against fabrication. Adds a regression test. Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> * fix(api): raise provider apiKey cap for cookie-based web providers (#6715) (#6759) * fix(cli): fall back to settings.json when Claude Code binary is unresolvable (#6701) (#6734) getCliRuntimeStatus() only ever answered `installed` from binary resolution (known install paths + where/which PATH search), so a stale PATH, moved binary, or uncatalogued install method reported "not found" even when ~/.claude/settings.json proved the CLI was installed and used before — regressing behind upstream 9router's checkClaudeInstalled(), which already falls back to the settings file when where/which fails. withSettingsFallback() (new src/shared/services/cliInstallFallback.ts, kept out of the frozen cliRuntime.ts to respect its file-size ceiling) restores that parity: only when the binary lookup's own reason is "not_found" (never for deliberate security rejections like unsafe/relative env overrides or symlink escapes) and the tool's settings file exists on disk. * fix(providers): honor explicit thinking.budget_tokens 0 in openai->gemini transform (#6813) (#6821) The transform forwarded the Claude-style thinking.budget_tokens into generationConfig.thinkingConfig.thinkingBudget, but the presence check was truthy (&& thinking.budget_tokens). An explicit budget_tokens: 0 — the natural way to disable thinking — is falsy, so it was dropped and the request fell through to the default thinkingConfig injection, making the model think despite an explicit request for zero. Use an explicit numeric check so 0 is honored as thinkingBudget 0; includeThoughts is only set for a non-zero budget. * fix(compression): reconcile outer vs per-engine token counts (#6488) (#6741) * fix(compression): reconcile outer vs per-engine token counts on degenerate output (#6488) Outer originalTokens/compressedTokens (real tiktoken counter over extracted message text) diverged from engineBreakdown[0]'s counts (a crude JSON.stringify(requestBody).length/4 estimate), worst on small/degenerate inputs where JSON structural overhead dominates. A single-engine breakdown entry represents the exact same before/after transformation as the overall response, so reconcileSingleEngineTokens() now overwrites that one entry's counts with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched. * chore(6741): resolve release sync — CHANGELOG.md restored to release tip, entry moved to changelog.d fragment (fragments-first) * fix(api): accept enableRenderers in RTK compression config schema (#6703) (#6757) * fix(db): break probe-failed/restore loop on large storage.sqlite (#6632) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); keeps only the author's changes. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * feat(cursor): add Opus 4.8, Fable 5, and Sonnet 5 model families (#6779) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); keeps only the author's cursor registry + test changes. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(translator): read PDF/video file attachments for Gemini/Antigravity and Claude (#6790) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); keeps only the author's translator + test changes. Co-authored-by: Wital <witalorocha216@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(codex): strip include from compact responses requests (#6805) * fix(codex): strip include from compact responses requests Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); keeps only the author's changes. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(6805): move include-strip assertion to standalone test file to keep executor-codex.test.ts under frozen size cap Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6769) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); keeps only the author's changes. Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(bootstrap): filter empty process.env values to prevent Docker env crash loop (#6828) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR) and the direct CHANGELOG.md edit (fragments-first); keeps only the author's bootstrap change. Co-authored-by: Andrian B. <andrewbalanesq@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(providers): update SenseNova Token Plan support (#6330) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); the author's constants/registry/snapshot deltas were re-applied cleanly onto the release tip. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(providers): classify 404 as MODEL_NOT_FOUND to stop retry storm (#6829) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR) and the direct CHANGELOG.md edit (fragments-first); the author's chatCore/errorClassifier deltas were re-applied cleanly onto the release tip. Co-authored-by: Andrian B. <andrewbalanesq@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(api): accept all catalog engines on compression PUT schema (#6792) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR). Resolved the release's OmniGlyph engine addition additively (types.ts/compression.ts kept both 'relevance' and 'omniglyph') and extended stackedPipelineStepSchema + STACKED_PIPELINE_ENGINE_INTENSITIES with the omniglyph branch so the ENGINE_CATALOG-parity test passes. Co-authored-by: Pitchfork-and-Torch <Pitchfork-and-Torch@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(api): point CLI health command at /api/monitoring/health (#6677) (#6717) * fix(api): point CLI health command at /api/monitoring/health (#6677) bin/cli/commands/health.mjs called GET /api/health, a route that was moved to /api/monitoring/health without updating the CLI; the top-level /api/health handler never existed on disk (only degradation/ and ping/ sub-routes). Point runHealthCommand()/runHealthComponentsCommand() at /api/monitoring/health and read its real payload shape (activeConnections, circuitBreakers: {open,halfOpen,closed}, memoryUsage) instead of the old nonexistent requests/breakers/cache/memory fields. * chore(6717): re-sync onto release tip; move CHANGELOG entry to changelog.d fragment (fragments-first) * chore(cursor): add Grok 4.5 effort/fast model IDs (#6774) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); keeps only the author's changes. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED (#6791) * fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); keeps only the author's changes. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * refactor(deepseek): extract done-terminator helper to keep frozen file under cap Extracts the FINISHED-drain scheduler and finish-once guard added for the [DONE] terminator fix (#6777) into a new deepseek-web-done-terminator.ts module, so deepseek-web.ts stays under its frozen line cap (1148). Behavior is unchanged. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Pitchfork-and-Torch <Pitchfork-and-Torch@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * feat(models): add capability override UI (#6727) * feat(models): add capability override UI Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); renumbered the migration 118 -> 119 to resolve the collision with 118_provider_param_filters.sql already on release/v3.8.47; the author's i18n/localDb deltas were re-applied cleanly onto the release tip. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(6727): import model-capability-overrides DB fns directly (not via localDb barrel) to keep localDb under file-size cap; aligns with anti-barrel convention * chore(db): satisfy known-symbols contract for modelCapabilityOverrides Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> * fix(cursor): use Agent CLI build id for x-cursor-client-version (#6795) * fix(cursor): use Agent CLI build id for x-cursor-client-version Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); the author's .env.example/docs deltas were re-applied cleanly onto the release tip. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(changelog): re-sync CHANGELOG.md to release tip (restore #6701 bullet) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(startup): rename reasoningControls.ts to avoid webpack casing collision (#6584) (#6718) * fix(startup): rename reasoningControls.ts to avoid webpack casing collision (#6584) * chore(6718): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(build): suppress Turbopack over-bundling warning from agentSkills generator (#6582) (#6720) * fix(build): suppress Turbopack over-bundling warning from agentSkills generator (#6582) generator.ts builds outputBase from a non-literal outputDir parameter, so Turbopack's file-tracing analyzer can't narrow it and emits an "Overly broad patterns" warning per entry point that imports the module (603 warnings on v3.8.46, up from 379). The fs access is legitimate and bounded, so next.config.mjs now suppresses this specific diagnostic via turbopack.ignoreIssue, mirroring the existing webpack.ignoreWarnings precedent in the same file. * chore(6720): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(providers): drop image_generation for Codex Spark models regardless of plan (#6651) (#6721) * fix(providers): drop image_generation for Codex Spark models regardless of plan (#6651) * chore(6721): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(providers): stop quota card re-sorting Codex/GLM bars by remaining % (#6687) (#6722) * fix(providers): stop quota card re-sorting Codex/GLM bars by remaining % (#6687) QuotaCardExpanded.tsx unconditionally re-sorted quotas by remaining percentage via sortQuotasByRemaining(), discarding the deterministic CODEX_QUOTA_ORDER/GLM_QUOTA_ORDER window order quotaParsing.ts's sortCodexOrder()/sortGlmOrder() had already established. A new hasFixedQuotaOrder() + resolveQuotaDisplayOrder() skip the re-sort for providers with a fixed window order (codex, glm family), threading providerId from QuotaCard.tsx through to the display layer. Regression guard: tests/unit/quota-card-expanded-fixed-order-6687.test.ts * chore(6722): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(startup): lazy-import ioredis in rateLimiter to fix MCP ERR_MODULE_NOT_FOUND (#6559) (#6725) * fix(startup): lazy-import ioredis in rateLimiter to fix MCP ERR_MODULE_NOT_FOUND (#6559) * chore(6725): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(resilience): resolve fp-pinned combo account back to real connection id (#6696) (#6732) * fix(resilience): resolve fp-pinned combo account back to real connection id (#6696) * chore(6732): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(api): Responses passthrough emits event-only SSE frames after filtering commentary output (#6561) (#6735) * fix(api): Responses passthrough emits event-only SSE frames after filtering commentary output (#6561) The #6199 commentary-drop `continue;` branches in stream.ts skipped the data: line for a dropped commentary event but never cleared the already-buffered event: line for the same frame, so the next blank line flushed the stale event: line alone -- an event-only SSE frame that crashes the OpenAI Python SDK's json.loads(). Both drop sites now call clearPendingPassthroughEvent() before continue. The commentary-drop decision was extracted into a new responsesCommentaryDrop.ts module so the fix does not grow the frozen stream.ts. * chore(6735): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(api): emit reasoning_content on claude-web + v0-vercel-web SSE (#6662) (#6743) * fix(api): emit reasoning_content on claude-web + v0-vercel-web /v1/chat/completions SSE (#6662) * chore(6743): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(sse): unwrap bare {function:{…}} tools in openai→claude translation (#6704) * fix(sse): unwrap bare {function:{…}} tools in openai→claude translation Some OpenAI-shape clients send a tool as a bare `{ function: {...} }` object, omitting the spec-required `type: "function"` parent wrapper. The tools-mapping in openai-to-claude.ts (~line 366) only unwrapped `tool.function` when `tool.type === "function"` was ALSO true, so a bare-function tool fell through to `toolData = tool` (the wrapper itself, with no `.name`), producing an empty `originalName` and silently dropping the tool from the translated request — worse than a 400, since the caller has no signal the tool never made it upstream. Unwrap `tool.function` whenever present, independent of the parent `type` field. Regression guard: tests/unit/openai-to-claude-bare-tool.test.ts. Co-authored-by: Samir Abis <me@samirabis.com> Inspired-by: https://github.com/decolua/9router/pull/2473 * chore(6704): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) --------- Co-authored-by: Samir Abis <me@samirabis.com> * fix(oauth): avoid bare-email dedup of Codex OAuth logins (#6706) * fix(oauth): avoid bare-email dedup of Codex OAuth logins When an incoming Codex OAuth connection has no verifiable workspace/account id, do not merge it into an existing row on email match alone — that silently overwrote the other account's token pair. Require a matching chatgptUserId (a stable per-account JWT id) before merging; otherwise insert a distinct connection row. Co-authored-by: lucasjustinudin <34107354+lucasjustinudin@users.noreply.github.com> Inspired-by: https://github.com/decolua/9router/pull/2477 * chore(6706): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) --------- Co-authored-by: lucasjustinudin <34107354+lucasjustinudin@users.noreply.github.com> * fix(sse): skip thinkingConfig for gemma models in openai→gemini translation (#6708) open-sse/translator/request/claude-to-gemini.ts already guards against sending thinkingConfig for gemma-4-* models (Gemma doesn't support it — Vertex returns 400: "Thinking budget is not supported for this model"), but the OpenAI-shape path (openai-to-gemini.ts) lacked the same guard, so OpenAI-shape clients hitting a vertex gemma-4-* model still got a 400. Mirrors the existing claude-to-gemini.ts guard: wrap the reasoning_effort and Claude-shape thinking.budget_tokens branches with a model.startsWith ("gemma-4") check. Branch 3 (default includeThoughts for modern Gemini models) already excludes non-"gemini" model ids and needed no change. Inspired-by: https://github.com/decolua/9router/pull/2480 Co-authored-by: chy1211 <31048289+chy1211@users.noreply.github.com> * fix(codex): surface capacity errors embedded in 200-OK SSE streams (#6710) * fix(codex): surface capacity errors embedded in 200-OK SSE streams Codex sometimes answers with HTTP 200 and a text/event-stream body whose payload carries a transient error mid-stream (e.g. "Selected model is at capacity...", server_is_overloaded, service_unavailable_error). Because the outer HTTP status was 200, this looked like a successful response to every caller — no retry, no circuit breaker, and no combo/account fallback ever engaged, so a healthy account sat idle while the request silently failed or truncated. Add peekCodexSseTransientError() to open-sse/executors/codex.ts: it peeks the first bytes of a text/event-stream Codex response, pattern-matches the known transient-error signatures, and converts a match into a real 503 Response via errorResponse() (Hard Rule #12 — sanitized, never raw upstream text). A 503 is already a recognized provider-failure status in accountFallback.ts, so combo routing and connection cooldown pick it up automatically. When no error signature is found, the peeked prefix is prepended back onto the remaining upstream body so the passthrough stays byte-identical to the unmodified response. Regression guard: tests/unit/codex-sse-capacity-fallback.test.ts — a model-at-capacity payload and a server_is_overloaded/service_unavailable_error payload both convert to 503; a normal single-chunk SSE stream and one split across multiple network chunks both reassemble byte-for-byte unchanged. Inspired-by: https://github.com/decolua/9router/pull/2452 (sub-bug #3 only — OmniRoute already covers PR #2452's other two sub-bugs: service_tier "fast" normalization and reasoning_effort "max" normalization). Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com> * chore(6710): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) --------- Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com> * fix(volcengine): clamp Kimi max_tokens to Ark endpoint cap (#6712) * fix(volcengine): clamp Kimi max_tokens to Ark endpoint cap VolcEngine Ark's Kimi coding-plan endpoint (ark.cn-beijing.volces.com) enforces max_tokens <= 32768 server-side and returns 400 "integer above maximum value, expected a value <= 32768" for anything over that ceiling. OmniRoute's StripRule only supported dropping params outright, with no numeric clamp mechanism, so a client sending a larger max_tokens (common default, e.g. 65536) 400s outright against volcengine's kimi-k2-5-260127. The 32768 cap is independently confirmed against two live-endpoint bug reports hitting this exact Ark endpoint for both kimi-k2.5 and kimi-k2.7-code (NousResearch/hermes-agent#51773, MoonshotAI/kimi-cli#1124), not just upstream's own value — same cap upstream 9router#2460 uses. StripRule gains two optional fields: `clampToModelMaxOutput` (clamp to the model's own catalog maxOutputTokens ceiling, when set) and `maxOutputCap` (a fixed endpoint-imposed ceiling); when both apply, the lower wins. The new rule is scoped to the literal id `kimi-k2-5-260127` (OmniRoute's real volcengine Kimi model, not upstream's `Kimi-K2.7-Code`), not a broad /kimi/i regex, so it can never clamp an unrelated future Kimi listing whose Ark cap may differ. glm-4-7-251222 (the other volcengine model) is unaffected. Inspired-by: https://github.com/decolua/9router/pull/2460 Co-authored-by: whale9820 <whale9820@users.noreply.github.com> * chore(6712): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) --------- Co-authored-by: whale9820 <whale9820@users.noreply.github.com> * fix(antigravity): surface aborted Gemini tool calls off end_turn (#6713) * fix(antigravity): surface aborted Gemini tool calls off end_turn Gemini/Antigravity aborts a turn with finishReason MALFORMED_FUNCTION_CALL (or a sibling like UNEXPECTED_TOOL_CALL) instead of completing cleanly. Both Claude-facing translators collapsed these to a clean end_turn, hiding the aborted tool call as a successful completion: - the OpenAI hub path (openai-to-claude.ts convertFinishReason default), and - the DIRECT Gemini->Claude path (gemini-to-claude.ts), which is the one Claude Code actually hits through an antigravity/Gemini-routed model. Add isAbortFinishReason() to finishReason.ts and map these reasons to tool_use on both paths; genuinely unknown reasons still fall back to end_turn. Co-authored-by: anhdiepmmk <n08ni.dieppn@gmail.com> Inspired-by: https://github.com/decolua/9router/pull/2462 * chore(6713): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) --------- Co-authored-by: anhdiepmmk <n08ni.dieppn@gmail.com> * fix(translator): strip empty cloud_base_branch from Cursor Subagent tool call (#6729) * fix(translator): strip empty cloud_base_branch from Cursor Subagent tool call (port from 9router#2446) The Responses->Chat tool-arg cleanup (stripEmptyOptionalToolArgs) only stripped empty-string/empty-array optional args for Claude Code's Read tool. Cursor's local Subagent tool call therefore passed through with the cloud-only field cloud_base_branch: "", which Cursor rejects ("cloud_base_branch may only be specified when environment equals cloud") before starting the subagent. Extend the cleanup to an allowlist of Read + Subagent; arbitrary tools stay untouched. Reported-by: like3213934360-lab (https://github.com/decolua/9router/issues/2446) * chore(6729): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) * fix(translator): defer content_block_start until GLM streams the tool name (#6730) * fix(translator): defer content_block_start until GLM streams the tool name (port from 9router#2077) GLM 5.2 (and similar OpenAI-compatible upstreams) stream a tool call's id and function.name across separate SSE delta chunks. The openai-to-claude streaming translator emitted content_block_start immediately on the id-only chunk with an empty name; the Claude SSE protocol cannot patch a block after emission, so the later name-only chunk was dropped and Claude Code rejected the tool_use with an empty tool name / "No such tool available:". Defer content_block_start until the name arrives (start on args if they arrive first), and emit a start for any orphaned id-only tool call at finish so content_block_stop is never orphaned. Reported-by: itiwant (https://github.com/decolua/9router/issues/2077) * chore(6730): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) * feat(dashboard): add search to Playground model picker dropdown (#4086) (#6811) * feat(dashboard): add search to Playground model picker dropdown (#4086) The shared ModelSelectModal (combo builder + CLI-code cards) already had search, but the Playground's raw model <select> in StudioConfigPane stayed a flat unsearchable list - unusable once a provider like OpenRouter contributed 50+ models. Adds a search input above the dropdown that filters options via filterModelsByQuery() (Turkish-safe accent/case-insensitive match, reusing matchesSearch()). The currently selected model always stays pinned in the list even when it doesn't match the query, so typing never silently swaps the active selection. Reuses the existing common.search i18n key already translated in all 42 locales - no new key needed. * chore(6811): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) * feat: request count log per provider, per date (#4009) (#6812) * feat(dashboard): request count log per provider, per date (#4009) Some providers bill by request rather than by token, so operators need a plain per-provider, per-date request count breakdown, not just token aggregates. Adds a new getProviderDailyUsageRows() aggregation query (src/lib/db/usageAnalytics.ts), a dedicated GET /api/usage/requests-by-provider-date route (kept separate from the frozen /api/usage/analytics route to respect the file-size baseline), and a sortable, single-date-filterable table on Dashboard -> Analytics. Closes #4009 * chore(6812): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) * feat(xai): route xAI clients to Grok native /v1/responses endpoint (#6709) * feat(xai): route xAI clients to Grok native /v1/responses endpoint xAI ships a native /v1/responses endpoint (https://api.x.ai/v1/responses) alongside /v1/chat/completions, but XaiExecutor extended BaseExecutor without overriding buildUrl(), so every request always resolved to the static chat-completions baseUrl regardless of target format — the last genuinely-missing slice of decolua/9router#2439 (grok-build-0.1, the reasoning-effort suffix routing, and bare grok-* routing were already ported in prior cycles). Add responsesBaseUrl to the xai registry entry and tag grok-4.20-multi-agent-0309 (upstream's own Responses-only id) with targetFormat: "openai-responses", mirroring the existing model-tag-driven routing pattern already used by the gh executor (9router#102) and the "openai" -pro heuristic in open-sse/executors/default.ts — the per-model registry tag is the single source of truth that also drives chatCore's body translation, so URL and body stay in lockstep. XaiExecutor.buildUrl now checks getModelTargetFormat("xai", model) and resolves to the native Responses endpoint only for tagged models, leaving every other grok-* model on the existing chat-completions bridge. TDD: tests/unit/executor-xai.test.ts adds a RED-then-GREEN case asserting grok-4.20-multi-agent-0309 resolves to https://api.x.ai/v1/responses and a control case asserting grok-4.3 still resolves to https://api.x.ai/v1/chat/completions. Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com> Inspired-by: https://github.com/decolua/9router/pull/2439 * chore(6709): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first) --------- Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com> * fix(resilience): route remaining credential-selection call sites through quota preflight (#6686) (#6742) * fix(resilience): route remaining credential-selection call sites through quota preflight (#6686) * chore(6742): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(resilience): apikey-provider 429s honor explicit quota-exhausted text (#6638) (#6731) * fix(resilience): apikey-provider 429s honor explicit quota-exhausted text (#6638) Ollama Cloud (and any other apikey-category provider) 429s skipped body-text quota classification entirely; a genuine multi-day quota exhaustion was misclassified as a plain rate_limit_exceeded with a few seconds of cooldown, so combo routing retried the account immediately. shouldPreserveQuotaSignals() now lets an explicit quota-exhausted signal (looksLikeQuotaExhausted) override the apikey-category default, and parseDayGranularityResetMs() adds day- granularity reset-hint parsing ("...reset in 3 days.") alongside the existing Xh/Ym/Zs parsing. Regression guard: tests/unit/issue-6638-ollama-quota.test.ts (RED before the fix, GREEN after). Aligned two tests/unit/account-fallback-service.test.ts cases that had codified the old buggy behavior for apikey-provider quota text. * chore(6731): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * feat(resilience): weekly-429 cooldown for fetcher-less providers (#3709) (#6817) * feat(resilience): weekly-429 cooldown for fetcher-less providers (#3709) Ollama Cloud free-tier accounts have a hard WEEKLY request cap. On cap the upstream returns 429 "you (<account>) have reached your weekly usage limit", but ollama-cloud is an apikey-category provider, so the existing oauth-only shouldUseQuotaSignal gate in checkFallbackError skips the subscription-quota-text classifier (Issue #2321) for its 429s -- the account fell through to the generic exponential backoff (~1s, capped at 2min) and got retried every few minutes for the rest of the week (one account took 285x429 in 48h). Adds a new, ungated weekly-usage-limit text classifier that applies a 24h QUOTA_EXHAUSTED cooldown regardless of provider category. Extracted the new classifier -- together with the existing #2321 subscription-quota logic -- into a new open-sse/services/quotaTextCooldowns.ts module so the frozen accountFallback.ts (file-size-baseline cap) didn't have to grow; net effect shrinks accountFallback.ts by 20 lines. This is Phase A of the plan (open-sse/services/accountFallback.ts:1038-1045 "weekly-429 cooldown"); Phase B (generic local request-counter preflight for manual provider_plans dimensions) is a separate, larger follow-up per the plan's own phasing. * chore(6817): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * fix(providers): Kiro adaptive-thinking allowlist excludes sonnet-4.5/haiku-4.5 (#6576) (#6726) * fix(providers): Kiro adaptive-thinking allowlist excludes sonnet-4.5/haiku-4.5 (#6576) * chore(6726): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first) * test(kiro): migrate selector-strip test to claude-sonnet-5 (only Kiro adaptive-thinking model, #6576) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(quality): rebaseline complexity 2053->2054 (merge-burst drift, v3.8.47) Inherited drift from today's /implement-prs merge burst (~36 PRs). check:complexity does not run on the PR->release fast-path, so the branch accrued +1 unmeasured. No orphan/feature PR introduces a NEW violation (complexity-net-zero); the only flagged function is the pre-existing getResolvedModelCapabilities. Owner-approved rebaseline to unblock the FQG of ~7 green-except-complexity orphans. * chore(stryker): register ollama-quota covering tests (merge-burst drift, v3.8.47) The 3 covering unit tests from #6731/#6817/#6742 (issue-6638-ollama-quota, ollama-cloud-weekly-quota-cooldown-3709, issue-6686-quota-preflight-coverage) exist on release but were never added to tap.testFiles when those PRs merged. Completes the registration so mutant kills count; unblocks every PR touching a mutated module. Part of the owner-approved merge-burst drift cleanup. * fix: auto-start WS server in-process and change default port to 20132 (#6072) * feat: change default LIVE_WS_PORT from 20129 to 20132 Update the default WebSocket port for the live dashboard server from 20129 to 20132 across all configuration files, documentation, code comments, and tests. Also consolidate OMNIROUTE_DISABLE_LIVE_WS and OMNIROUTE_ENABLE_LIVE_WS into a single OMNIROUTE_ENABLE_LIVE_WS flag. Wire the live WebSocket server to start in-process via instrumentation-node.ts. * feat: clarify NEXT_PUBLIC_LIVE_WS_PUBLIC_URL path usage and derive upgrade path from URL Update .env.example and ENVIRONMENT.md to document that the pathname portion of NEXT_PUBLIC_LIVE_WS_PUBLIC_URL (e.g. /live-ws) is used as the WebSocket upgrade path by the dev proxy, handshake response, and client connection logic. Extract deriveLiveWsPath() into shared/utils/wsPath.ts and wire it through: - src/app/api/v1/ws/route.ts — handshake response path field - src/hooks/useLiveDashboard.ts — build * fix: use the standard URL API to safely parse and update the effectiveWsUrl * build(docker): expose live WebSocket server port and configure CORS origins Add LIVE_WS_PORT (20132), LIVE_WS_HOST (0.0.0.0), and LIVE_WS_ALLOWED_ORIGINS environment variables to all Docker Compose profiles and expose the WebSocket port mapping. Prevent infinite self-loop in standalone-server-ws.mjs by skipping proxy when the server itself is running on the LiveWS port. * docs(env): fix comment formatting for HOST and HOSTNAME variables --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(logs): prevent stale detail refresh reopening modal (#6323) * fix(logs): prevent stale detail refresh reopening modal * chore(stryker): register ollama-quota covering tests (release drift from merge burst) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * \ feat: operator-configurable account rotation\ (#6763) * feat(resilience): operator-configurable account rotation Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); the author's accountFallback/.env deltas were re-applied cleanly onto the release tip. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * docs(env): document configurable account-rotation env vars in ENVIRONMENT.md Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * refactor(rotation): extract rotation gate/context helpers to keep accountFallback.ts under frozen cap Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(changelog): re-sync CHANGELOG.md to release tip (restore lost base bullet) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * test(stryker): register rotation-config test in tap.testFiles for mutation coverage Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * test(stryker): register ollama-quota covering tests (drift from #6731/#6817/#6742) + re-sync Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> * fix(lmarena): modernize Arena web provider + static Direct-chat catalog (#6280) * fix(lmarena): modernize Arena web provider + static Direct-chat catalog Update the lmarena provider for arena.ai (product rebranded from LMArena): - Route chat via arena.ai create-evaluation with Chrome TLS impersonation (tls-client-node) and optional browser-minted recaptchaV3Token. - Seed Text+Search (48) into the chat registry; seed Image (27) only into IMAGE_PROVIDERS. Disable live HTML model discovery; resolve public names to Arena UUIDs from the static TypeScript allowlist (no scrape JSON in-repo). - Soft-exclude 404/502 model ids; slow/stop bulk test-all probes for this provider. - Do not fold IMAGE_PROVIDERS/video specialty into the chat provider catalog when a chat registry already exists (lmarena/openai/xai). - Display name Arena (Free); keep wire id `lmarena` / alias `lma` for back-compat. - Theme-aware provider icons: arena-light.svg / arena-dark.svg. - Preserve split Supabase SSR cookie reconstruction for arena-auth-prod-v1.*. * fix(providers): align provider-models-route test fixture + regen provider reference Fold the topaz image-only catalog entry's apiFormat/supportedEndpoints into the local-catalog test fixture (route now tags media-only providers per the lmarena PR's staticModels.ts change), regenerate PROVIDER_REFERENCE.md against the merged release providers.ts, and add the changelog fragment for #6280. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * test: align web-cookie fallback suite — lmarena now has a registry entry (probe path) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * docs(changelog): reconcile 3-day merge burst — 16 fragments, 4 promised credits, contributors hall 32→63 - changelog.d fragments for the 20 merged PRs that landed without a bullet (#6072 #6308 #6323 #6538 #6556 #6586 #6611 #6647 #6675 #6698 #6757 #6759 #6804 #6821 + ci rollup #6781/#6691/#6693 + docs rollup #6643/#6644/#6646/#6663; omniglyph bump #6661 folded into the #6556 bullet) - deliver the 4 credits promised in close comments but never written: @alltomatos (#6819 dup of #6721), @samimozcan (#6762/#6753 subsumed by #6790), @chirag127 (#6756 dup of #6757), @Squawk7777 (#6565 dup of #6564 — appended to the existing #6564 bullet; changelog-integrity flags that edit as a removal, intentional: ALLOW_CHANGELOG_REMOVALS justification) - rebuild the v3.8.47 Contributors hall from merged-PR authors + thanks credits + prior hall: 32 → 63 contributors * Clamp reasoning token buffer to model output cap (#6714) * fix(combo): clamp reasoning buffer to model output cap * fix(routing): preserve near-cap reasoning max tokens * fix(routing): getExplicitModelOutputCap falls through to registry cap on non-numeric synced limit_output getExplicitModelOutputCap short-circuited to null whenever a synced capability row existed, even if that row's limit_output was not a number (models.dev commonly omits it). That silently disabled the reasoning-token buffer clamp for any model with a synced row lacking an output limit. Now only return the synced value when it IS a number; otherwise fall through to registryModel.maxOutputTokens / spec.maxOutputTokens, matching the ??-chain precedence already used by getResolvedModelCapabilities(). Adds a standalone regression test (proves the fallthrough returns the real registry cap, not null) and hardens the #6274 fixture id so its no-output-cap case does not prefix-match the real glm-5.2 static spec. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(stryker): register ollama-quota covering tests (release drift from merge burst) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * feat(i18n): add Traditional Chinese (zh-TW) localization for frontend and CLI (#6320) * feat(i18n): add Traditional Chinese (zh-TW) localization for frontend and CLI - Add src/i18n/messages/zh-TW.json translating frontend web UI - Add bin/cli/locales/zh-TW.json translating CLI commands and descriptors - Register zh-TW in config/i18n.json and docs/guides/I18N.md - Update scripts/i18n/generate-multilang.mjs matching the new locale setup * fix: update i18n locale count from 42 to 43 after adding zh-TW The docs strict checker (check-docs-counts-sync.mjs) validates that README.md and I18N.md reflect the real locale count. Adding zh-TW bumped the count from 42 → 43. * fix(i18n): translate providers free-filter labels in zh-TW (#6694 guard) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: lunkerchen <lunkerchen@users.noreply.github.com> * feat(proxy): implement latency-optimized proxy rotation strategy (#6798) * feat(proxy): implement latency-optimized proxy rotation strategy Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR) and the direct CHANGELOG.md edit (fragments-first); the author's env/docs/i18n deltas were re-applied cleanly onto the release tip. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(proxy): add latency-rotation env var to .env.example PROXY_LATENCY_WINDOW_HOURS was referenced in src/lib/db/proxies.ts and documented in docs/reference/ENVIRONMENT.md, but missing from .env.example, tripping the env/docs sync gate. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(changelog): re-sync CHANGELOG.md to release tip Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * refactor(proxy): extract latency-strategy helpers to keep frozen files under cap Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * test(db-rules): expect 35 audited modules (proxyLatency joins INTENTIONALLY_INTERNAL) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * docs(readme): fix stale strategy/tool/scoring counts (#6853) README still claimed 17 routing strategies (the table was missing pipeline), 95 MCP tools, and 9-factor Auto-Combo scoring. Align with the source (ROUTING_STRATEGY_VALUES has 18 entries) and the canonical docs (MCP-SERVER.md: 94 tools; AUTO-COMBO.md: 12-factor). * fix(antigravity): sanitize Cloud Code safety settings (#6839) Co-authored-by: kfiramar <83420275+kfiramar@users.noreply.github.com> * fix(kiro): probe IdC region during profileArn discovery, cross-region (recovers #6099) (#6840) * fix(kiro): route Amazon Q runtime by profileArn region for cross-region IdC Enterprise AWS IAM Identity Center accounts whose IdC instance lives outside the two Amazon Q Developer profile regions (us-east-1 / eu-central-1) - e.g. eu-north-1 (Stockholm), start URL https://d-XXXX.awsapps.com/start - showed no limits and returned 502 on every request. Root cause: the backend used the IdC/OIDC token region (providerSpecificData.region, e.g. eu-north-1) for every CodeWhisperer runtime call, hitting q.eu-north-1.amazonaws.com - a host that does not exist as a Q Developer runtime endpoint. Per AWS docs ("Supported Regions for the Q Developer console and Q Developer profile"), the Q Developer *profile* (which produces the profileArn and hosts generateAssistantResponse / GetUsageLimits / ListAvailableModels / ListAvailableProfiles) is only hosted in us-east-1 and eu-central-1, regardless of the IdC region; "data is stored in the Region where you create the Amazon Q Developer profile." Fix (new open-sse/services/kiroRegion.ts) decouples the two regions: - providerSpecificData.region stays the IdC/OIDC region, used ONLY for oidc.{region}.amazonaws.com token mint/refresh. - The runtime region is derived from the profileArn (resolveKiroRuntimeRegion): profileArn region -> a valid stored profile region -> us-east-1. A stored IdC region that is not a Q profile region (eu-north-1) is ignored for runtime. - Profile discovery (discoverKiroProfileArnAcrossRegions) probes the Q profile regions (EU IdC -> eu-central-1 first) with the cross-region SSO token instead of q.{idcRegion}. Wired into: executors/kiro.ts (generateAssistantResponse targets the profile region), services/usage/kiro.ts (getKiroUsage multi-region discovery + profileArn runtime region so Limits resolves), services/kiroModels.ts (ListAvailableModels), and src/lib/oauth/providers/kiro.ts (login-time postExchange profile discovery). Adds tests/unit/kiro-idc-cross-region.test.ts (15 cases). All Kiro suites pass (60 tests). * fix(kiro): probe the IdC region too during profileArn discovery (any IdC region) Make profile discovery general for an IdC in ANY of the ~30 IdC-supported AWS regions (us-west-2, ap-southeast-2, me-central-1, af-south-1, ...), not just eu-north-1. buildKiroProfileDiscoveryRegions now probes the two documented Q Developer profile regions FIRST (us-east-1 / eu-central-1, EU-first for EMEA IdC regions to cut latency), then appends the IdC/stored region itself as a forward-compatible fallback: if AWS ever co-locates the profile with the IdC or expands the profile-region list, a same-region probe still finds it. Probing a region with no profile simply returns nothing and we fall through. The profileArn's own region remains authoritative for every runtime call (resolveKiroRuntimeRegion), so a newly-issued ARN in any region is honored automatically. Adds ap-southeast-2 (APAC) cross-region coverage and updates the discovery-order tests. --------- Co-authored-by: artickc <artur1992123@mail.ru> * feat(providers): manual context-window override for custom models (#4125) (#6822) Add a manual per-model "Context Window Override" so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model getting silently dropped from combo routing once the wrong value lands in the catalog. Reuses the existing Feature-5004 model_context_overrides table (source="manual") — already the priority-0 source getModelContextLimit() (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: - PUT /api/provider-models now accepts an optional contextWindowOverride (number to set, null to clear), persisted via setModelContextOverride/ removeModelContextOverride. - GET /api/provider-models surfaces the current override value + source back on each custom-model row. - CustomModelsSection.tsx: edit form gained a Context Window Override input + a badge on the model row when an override is set. Regression guard: tests/unit/provider-models-context-window-override-4125.test.ts (manual override wins over a misreported catalog value, GET round-trip, clearing via null, default-unchanged behavior). * feat(dashboard): improve Provider Quota page horizontal density (#3520) (#6815) QuotaCardGrid stacked every provider group vertically in a single flex flex-col container, and each group's own card grid didn't go multi-column until the md breakpoint. Provider groups now flow into a 2-column CSS multi-column layout on very wide (2xl) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately, filling horizontal whitespace sooner on narrower-but-not-mobile viewports. Regression guard: tests/unit/quota-card-grid-horizontal-layout.test.ts * refactor(usage): type saveRequestUsage with UsageEntry interface + any-budget ratchet (#3512) (#6809) Replace saveRequestUsage(entry: any) with a typed UsageEntry interface mirroring the usage_history columns 1:1. Fields stay optional/nullable since different writers (chatCore success/failure, rejected-request accounting, Codex Responses WS) populate the row incrementally; tokens stays unknown since callers pass either raw provider-shaped usage or the normalized {input,output,cacheRead,...} shape. Also cleaned the file's other any usages (getUsageHistory filter, getUsageDb next-cursor cast, appendRequestLog tokens param, getRecentLogs catch) so it now sits at zero any and can be added to the check:any-budget:t11 zero-any allowlist. Documents the DB-entity <-> TS-interface convention in docs/architecture/CODEBASE_DOCUMENTATION.md Sec 11. * feat(combo): strict budget-cap fallback policy for auto/* combos (#3470) (#6816) Auto-combo transparency + budget controls: the engine's budgetCap enforcement always degraded to the globally cheapest candidate when every candidate exceeded the cap - silently overspending instead of respecting the cap. - engine.ts: budgetFallback "cheapest" (default, legacy) | "strict" (BudgetExceededError when no candidate fits budgetCap) - requestControls.ts: X-OmniRoute-Budget-Fallback header + resolveRequestAutoControls() consolidating mode/budget/fallback parsing - resolveAutoStrategy.ts / autoConfig.ts: thread combo-level config.budgetFallback and catch BudgetExceededError into an HTTP 402 - chat.ts: switch to the consolidated resolveRequestAutoControls() helper (net line reduction, stays under the frozen file-size baseline) Regression guard: tests/unit/auto-combo-budget-fallback-3470.test.ts * fix(usage): honor xAI provider-reported exact cost (#6711) OmniRoute's calculateCost() always estimated request cost from token counts x static pricing, discarding xAI's exact provider-reported cost when present. xAI's chat-completions usage object reports the precise billed cost via cost_in_usd_ticks (docs.x.ai/developers/cost-tracking and the API reference's usage schema: "TICKS_IN_USD_CENT: i64 = 100_000_000" => 1e10 ticks/USD, e.g. 37756000 ticks ~= $0.0038). calculateCost()/computeCostFromPricing() now short-circuit to this exact figure when present -- before any pricing DB lookup, so it also works for models without a local pricing row -- and still fall back to the token-based estimate when it is absent. The field is threaded through both the streaming (extractUsage/normalizeUsage) and non-streaming (extractUsageFromResponse) usage-extraction paths. Corrected divisor vs upstream: the upstream PR used /1e12 (a 100x under-report, e.g. reporting $0.00123 as the doc's $0.123 example); this port uses the doc-verified /1e10 instead, confirmed against both the cost-tracking guide and the API reference's usage-object schema. Inspired-by: https://github.com/decolua/9router/pull/2453 Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com> * docs: rename /implement-prs → /merge-prs in Hard Rule #21 (skill renamed 2026-07-11) (#6847) * docs: refresh stale llm.txt facts + relocate design.md to docs/architecture/DESIGN_SYSTEM.md (#6849) * docs: refresh stale llm.txt facts + move design.md to docs/architecture/DESIGN_SYSTEM.md llm.txt was frozen at the v3.8.8 era (177 providers, 37 MCP tools, 14 strategies, 9-factor scoring, 75% coverage gate). Update every factual claim to the current state (248 providers, 94 tools / 30 scopes, 18 strategies, 12-factor scoring, ratchet + 60% floor, TS 6, current docs/ layout) and re-sync the 42 exact-copy i18n mirrors. design.md at the root was a standardization plan whose phases 1-6 all shipped; rewrite its header as a permanent reference and relocate it to docs/architecture/DESIGN_SYSTEM.md per the root-hygiene policy (root = configs + canonical docs only). * docs: add MDX frontmatter to DESIGN_SYSTEM.md (in-app docs pipeline requires it) * feat: per-model web-search interception rule (#3384) (#6814) * feat(routing): per-model web-search interception rule (#3384) Adds a per-provider/per-model interceptSearch rule (src/lib/db/interceptionRules.ts, key_value namespace interception_rules) that overrides the existing native web-search bypass defaults (Codex/Gemini/Claude->Claude passthrough) in webSearchFallback.ts. Wired at the existing prepareWebSearchFallbackBody() call site in chatCore.ts. Resolution precedence: per-model rule > provider-level rule > existing native-bypass defaults. This lands Phase 1-2 of the plan (rule store + search interception). Web-fetch interception and the dashboard UI toggle are tracked as follow-up phases. * fix(db): register interceptionRules in localDb re-export layer (db-rules gate) * fix(db): renumber interception_rules migration 119→120 (collision with model_capability_overrides) * feat: sidebar search/filter input (#4013) (#6810) * feat(dashboard): add search/filter input to the dashboard sidebar (#4013) Adds a search box at the top of the expanded sidebar that filters nav sections/groups/items client-side by label, so users don't have to hunt through the growing nav tree. Reuses the existing common.search / common.noResults i18n keys (no new locale edits needed) and the shared Input icon="search" pattern. Matching sections auto-expand while searching and the accordion/pin state is restored once the query is cleared. Filtering logic is extracted into a pure filterSidebarSectionsByQuery() helper (src/shared/utils/sidebarSearch.ts) so it is trivially unit testable independent of React/next-intl/next-navigation. * fix(test): move Sidebar.search test to a runner-collected path (test-discovery gate) * fix(i18n): backfill 194 missing pt-BR keys (#6695) (#6723) * fix(i18n): backfill 194 missing pt-BR keys and add key-parity regression test (#6695) * Merge branch 'release/v3.8.47' into fix/6695-i18n-drift Resolve i18n key-parity and CHANGELOG-fragment conflicts: - Convert the #6695 CHANGELOG.md bullet to a changelog.d/ fragment (the fragment convention landed on release/v3.8.47 after this PR branched, per changelog.d/README.md). - Backfill 61 additional pt-BR keys that entered en.json on release/v3.8.47 after this PR's original 194-key backfill, so the PR's own key-parity regression test (tests/unit/i18n-pt-br.test.ts) stays green against the moving release baseline. * Discover live Codex models (#6776) * Add live model discovery for provider catalog * Fix model discovery request headers * fix(codex): sync live model limits with local catalog * test(codex): split live model discovery coverage into dedicated route tests * fix(codex): use chatgpt account id for live model sync * Add GitHub-backed Codex model discovery fallback * fix(providers): tighten oauth config tests and provider model display comments * test: align client version expectations with release default * fix(codex): keep discovery complexity within baseline * fix: rebase live Codex model discovery onto release/v3.8.47, preserving kimi-web buildHeaders (#6308) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * feat(codex): echo requested effort-suffixed model id in Responses payloads (#3697) (#6820) * feat(codex): echo requested effort-suffixed model id in Responses payloads (#3697) Codex CLI compatibility shim: the Responses API response.created/ response.in_progress/response.completed payloads now carry a `model` field (previously absent), and for Codex-CLI-originated requests it echoes the client-requested effort-suffixed model id (e.g. gpt-5.5-xhigh) instead of the bare upstream id (gpt-5.5), so the Codex CLI status line/model button shows the active reasoning effort. - openai-responses.ts translator threads the upstream model into the Responses event objects (additive, omitted when unknown). - New isCodexOriginatedHeaders() (codexIdentity.ts) reuses PR #3481's originator/User-Agent detection, header-based so it still fires when a combo routes codex/gpt-5.5-xhigh to a non-codex upstream. - chatCore's existing opt-in #1311 echoModel pipeline now also fires automatically for Codex clients on the Responses API, regardless of the echoRequestedModelName setting. - responseModelEcho.ts now also rewrites the nested response.model field the Responses API uses (previously only top-level model). - /v1/models keeps returning models: [] for Codex (unchanged, #3481). Regression guard: tests/unit/codex-effort-model-echo-3697.test.ts. Closes #3697 * chore(merge): re-sync with release/v3.8.47 (restore CHANGELOG, keep own bullet) * chore(merge): re-sync with release/v3.8.47; move changelog bullet to changelog.d fragment (merge-storm proof) * feat(usage): surface Antigravity weekly quota alongside the 5-hour window (#4017) (#6818) * feat(usage): surface Antigravity weekly quota alongside the 5-hour window (#4017) Antigravity enforces both a 5-hour and a weekly usage limit, but the agy/antigravity quota widget only exposed the 5-hour window. The weekly limit isn't in the per-model retrieveUserQuota response already fetched — it lives in a separate, undocumented retrieveUserQuotaSummary RPC that groups models into families (Gemini Models, Claude and GPT models) with one weekly bucket per family. Adds a self-contained usage/antigravityWeeklyQuota.ts leaf: a cached, best-effort fetch of that RPC + a pure parser that extracts the weekly-labeled bucket per group (window inferred from bucketId/displayName text, matching the reverse-engineered shape documented by third-party Antigravity clients) into gemini_weekly/ claude_gpt_weekly quota entries, merged into the existing quotas map the widget already renders generically. A failed/unavailable RPC never affects the existing per-model quotas. Live VPS validation attempt (192.168.0.15, real antigravity account): both retrieveUserQuota and retrieveUserQuotaSummary currently return 429 RESOURCE_EXHAUSTED for that account, so the live response shape could not be captured directly. The parser was instead validated via TDD against the bucket shape documented by CodexBar (steipete/CodexBar), a third-party Antigravity client that reverse-engineered the same RPC, and is defensive against both response envelopes it has observed (top-level groups[] and nested quotaSummary.groups[]). * chore(merge): re-sync with release/v3.8.47 (restore CHANGELOG, keep own bullet) * chore(merge): re-sync with release/v3.8.47; move changelog bullet to changelog.d fragment (merge-storm proof) * feat: add Z.ai Web free web-cookie provider (#4056) (#6823) * feat(providers): add Z.ai Web free web-cookie provider (#4056) New zai-web web-session provider drives the free chat.z.ai consumer chat UI via a pasted browser cookie, distinct from the existing API-key zai/glm/glm-cn/glmt providers (api.z.ai). ZaiWebExecutor posts to chat.z.ai/api/chat/completions with the cookie forwarded both as Cookie and Authorization: Bearer <token>, and normalizes both z.ai's internal delta_content/phase SSE envelope and a pass-through OpenAI-shaped choices[].delta frame into standard chat-completion chunks. Registered in WEB_COOKIE_PROVIDERS, WEB_SESSION_CREDENTIAL_REQUIREMENTS, the provider registry (GLM-4.6/4.5/4.5V models), the executor factory, and tokenExtractionConfig.ts for in-app cookie capture. * fix(providers): regenerate translate-path golden for zai-web + reduce cognitive complexity * fix(providers): rename ZaiWebExecutor.buildHeaders to avoid incompatible BaseExecutor override * chore(merge): re-sync with release/v3.8.47; move changelog bullet to changelog.d fragment (merge-storm proof) * fix(codex): bump default client version to 0.144.0 (#6780) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * refactor(usage): extract per-group parsing in antigravityWeeklyQuota (cognitive-complexity gate 886→885, release-level drift from #6818 merge) * ci(quality): cut PR gate wall time without dropping protection (#6716) Collapse duplicate CI spend while keeping each gate's existence reason: - quality.yml: TIA __RUN_ALL__ defers full unit to fast-unit 4-shard (#6781); path filters via classify-pr-changes; docs-gates split; draft skip - ci.yml: wire docs/i18n/code path filters; ESLint JSON artifact for quality-gate; drop advisory typecheck:noimplicit; float actions/cache@v6 - TIA parity: memory/usage/combo/serial; **/*.test.mjs any depth; electron/bin no longer force unit __RUN_ALL__ - check:complexity-ratchets: one ESLint walk, ruleId-isolated baselines + cache - check:api-docs-refs + lib/apiRoutes: shared API route inventory - husky pre-push: intentionally light (gates live in pre-commit); CLAUDE.md + QUALITY_GATES.md docs synced - collect-metrics / lint:json: path.resolve cache path; Windows-safe eslint bin - env-doc allowlist for ESLINT_RESULTS_JSON / COMPLEXITY_ESLINT_REPORT - release-green --full-ci expects check:api-docs-refs (not docs-symbols alone) Tests: select-impacted, classify-pr-changes, api-routes lib, complexity-rule-count, validate-release-green. Reconciled after #6781 (fast-unit 2→4 shards) per maintainer request on #6716. Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * fix(docs): document Turbopack build memory tradeoff for RAM-constrained machines (#6409) (#6885) * fix(routing): recognize Kimi token-limit 400 as context overflow for combo fallback (#6637) (#6893) combo.ts's isContextOverflow400() guard required the literal word 'context' in the 400 error body before letting a combo fall through to the next target. Kimi's exact wording ('Your request exceeded model token limit: 262144 (requested: 308458)') never says 'context', so the guard misclassified it as a body-specific error and halted the whole combo instead of trying the next (larger-context) target. accountFallback.ts's CONTEXT_OVERFLOW_PATTERNS already recognized this wording one layer below (via checkFallbackError -> shouldFallback), so the two independently-maintained classifiers disagreed and the stricter one won. Export CONTEXT_OVERFLOW_PATTERNS from accountFallback.ts and reuse it inside combo.ts's isContextOverflow400() so both layers share a single source of truth. Regression test: tests/unit/repro-6637-kimi-token-limit.test.ts (RED on unfixed code -> GREEN after the fix). Existing #4519 guard tests (tests/unit/combo-param-validation-fallback-4519.test.ts) still pass, including the negative case that a genuinely body-specific 400 is NOT misclassified as overflow. * fix(providers): honor a provider-level proxy assigned to no-auth providers (#6272) (#6895) No-auth providers (mimocode, opencode, ...) are always dispatched with a single hardcoded connectionId ("noauth" — SYNTHETIC_NOAUTH_CONNECTION_ID in src/sse/services/auth.ts). No provider_connections row ever has id="noauth", so resolveProxyForConnection() in src/lib/db/settings.ts could never populate connectionRecord for them, and its provider-level proxy lookup (Steps 6/8) only runs when connectionRecord is present. A proxy assigned via Settings -> Providers -> mimocode was therefore silently ignored, reproducing the reporter's "same thing happen when i set the proxy directly in the provider menu" symptom. Adds a best-effort fallback (src/lib/db/settings/noAuthProxyFallback.ts): when connectionRecord could not be resolved, scan the known no-auth provider ids for a configured provider-level proxy (registry first, then legacy) before falling through to the global/direct steps. Regression test: tests/unit/proxy-noauth-provider-6272.test.ts (RED on unfixed code — resolved to level=direct/proxy=null; GREEN after the fix). * fix(dashboard): surface Claude extraUsage credits in quota card (#6806) (#6896) Enterprise-tier Claude accounts (default_raven_enterprise) don't get five_hour/seven_day utilization windows from Anthropic's OAuth usage endpoint — only an extra_usage credit-billing block. parseClaude() only read data.quotas, so quotas stayed {} and the dashboard showed "No quota data" even when extraUsage showed the account 100% exhausted. parseClaude() now folds an enabled extraUsage block into a credits-style quota row (mirroring parseCodex's bankedResetCredits pattern), both when quotas is empty and when it's already populated. * fix(db): share sql.js preinit across callers, fix named-param bind (#6628, #6802) (#6899) - preInitSqlJs() now memoizes an in-flight Promise (not just the resolved adapter) per filePath, so concurrent BATCH/STARTUP/HealthCheck/ ProviderLimitsSync callers at boot share one full-file read+WASM decode instead of each independently reloading the whole database — the thundering-herd amplifier of the OOM condition #6632 already partly fixed, left un-implemented by the reporter's own proposed fix (#6628). - sqljsAdapter's run/get/all now unwrap a lone named-parameter object (e.g. .all({ isActive: 1 }) for "WHERE is_active = @isActive", the same call shape getProviderConnections() already uses against better-sqlite3) before calling sql.js's stmt.bind(), expanding it to the @/:/$ sigil variants sql.js's own named-bind path requires. Previously the object was wrapped into an array and sql.js took the positional-bind path, throwing "Wrong API use : tried to bind a value of an unknown type ([object Object])." whenever the sql.js WASM fallback driver was active — exactly the error #6802 reported (misattributed to better-sqlite3). Regression tests added to tests/unit/db-adapters/driverFactory.test.ts and tests/unit/db-adapters/sqljsAdapter.test.ts, both proven RED against the prior code and GREEN after the fix. * fix(plugin): split OC-gate provider id from OmniRoute-facing routing id (#6859) (#6900) resolveOmniRoutePluginOptions() auto-prefixes providerId with "opencode-" (commit75b52e286) so OpenCode 1.17.8+'s native-adapter gate accepts it as a registered provider id. That prefixed value was being reused for the OmniRoute-server-facing identifiers too: mapRawModelToModelV2's id/providerID, mapComboToModelV2's providerID, and the dynamic provider hook's combo catalog keys. OmniRoute's server has no "opencode-<x>" provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" / "No active credentials for provider: opencode-omniroute". Add a second, unprefixed omnirouteProviderId field and thread it through the four dynamic-hook call sites that emit server-facing identifiers, while leaving the OC-gate-prefixed providerId in place for AuthHook.provider, provider registration (hook.id), and the static-catalog path (which OC strips before dispatch, per the existing static-block comments). * fix(compression): surface silently-dropped stacked-pipeline steps and fix inflation-guard no-op misfire (#6479, #6480, #6491) (#6901) Two related root causes in the stacked compression pipeline: - #6479/#6491: a dispatched step whose engine legitimately finds nothing eligible (session-dedup with no repeated blocks, ccr below its min-chars threshold) returns `{ stats: null }`. `mergeStackStep()` silently dropped that step from `engineBreakdown` with zero trace — no warning, no error. Now records a `"<engine>: skipped (no eligible content)"` validation warning for any null-stats step, covering every engine that follows this convention (session-dedup, ccr, headroom, relevance, llm, llmlingua, ionizer, readLifecycle), not just the two reported. - #6480: `finalizeStackedResult` ran the aggregate `guardPipelineInflation` check unconditionally, even when the loop-level `compressed` flag stayed false (no step ever advanced `currentBody`). Since tokens are trivially equal when nothing ran, the guard mislabeled a genuine no-op as `fallbackApplied: true` with a misleading "reverted to original" warning. Extracted the guard into `applyStackedInflationGuard()` in `pipelineGuards.ts` (keeps `strategySelector.ts` under its frozen line budget) and gated it on `compressed === true`. Also fixes `compression-pipeline-inflation-guard.test.ts`'s wire test, which passed a bare engine-id string to the pipeline; `normalizePipelineStep()` only recognizes a fixed set of built-in string aliases and silently downgrades any other string to `{ engine: "caveman" }`, so the test's custom inflating engine was never actually exercised. Passing a step object restores the test's original intent. New regression tests: tests/unit/compression/repro-6479-6491-null-stats-silent-drop.test.ts, tests/unit/compression/repro-6480-noop-guard-misfire.test.ts. * fix(mcp): de-duplicate TOTAL_MCP_TOOL_COUNT by tool name (#6854) (#6902) TOTAL_MCP_TOOL_COUNT in open-sse/mcp-server/server.ts summed collection sizes additively, double-counting tools registered in more than one collection. The agent-skills trio (omniroute_agent_skills_list/get/coverage) is intentionally defined in both MCP_TOOLS (schemas/tools.ts) and agentSkillTools (tools/agentSkillTools.ts), inflating the reported count from 96 unique tools to 99. Replace the additive sum with countUniqueMcpTools() (new open-sse/mcp-server/toolCount.ts), which unions all collection tool names into a Set before counting, so any future overlap self-corrects instead of double-counting. Regression test: tests/unit/mcp-tool-count-dedup-6854.test.ts * fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) (#6903) * fix(providers): honor max_token capability override in reasoning buffer clamp (#6524) (#6904) getExplicitModelOutputCap() (the clamp ceiling used by resolveReasoningBufferedMaxTokens) only ever read the unvalidated synced limit_output / registry / static-spec chain — it ignored the operator-settable max_token capability override that getResolvedModelCapabilities() already consulted. When a provider's synced catalog row reports a wrong limit_output (e.g. ollama-cloud/deepseek-v4-flash: limit_output=1048576, same as limit_context, while the real upstream cap is 65536), the reasoning-buffer clamp trusted the bad number and inflated max_tokens 64000 -> 96000, which upstream rejected with "exceeds model's maximum output tokens (65536)". The override table (model_capability_overrides, "max_token" key, /api/model-capability-overrides) is the existing, already-shipped remediation path for exactly this class of bad catalog data, but reasoningTokenBuffer.ts had no way to benefit from it. Extracted the override lookup into a shared getMaxTokenCapabilityOverride() helper and made getExplicitModelOutputCap() consult it first, so both read paths now agree. * fix(api): merge id-only tool_call continuation deltas in stream summary (#6276) (#6905) * fix(dashboard): logs detail modal no longer reopens on first close (#6830) LogsPage recomputed initialId from window.location on every render, but the App Router syncs window.location only after the navigation commits. Closing the detail modal re-rendered the page while the URL still carried ?id=X, so initialSelectedId flipped null -> X and the child's one-shot deep-link effect (guard still unarmed after the open-click render, where location was stale in the other direction) reopened the modal. Only the second close worked. Read the id once via lazy useState so the prop stays stable for the page's lifetime; deep links still open the modal on mount. Regression test reproduces the App Router ordering with a router.replace mock that re-renders the page before committing the URL. * fix(fusion): select judge from a surviving panel member when no explicit judge (#6869) When no explicit judgeModel is configured, the judge defaulted to panel[0] before fan-out and was never reassigned. If panel[0] failed fan-out (timeout / rate-limit / dropped straggler → it lands in `failures`, not `answers`), the multi-answer synthesis path still dispatched the judge to that dead panel[0], erroring the whole fusion request even though a quorum of other panel members succeeded — exactly the failure fusion exists to tolerate. Resolve the effective synthesis judge from a survivor when no explicit judge is set: prefer panel[0] only when it survived, otherwise the first surviving answer. An explicitly configured judge is still honored unchanged (operator intent), and the answers.length===0 (503) and single-survivor branches keep their existing semantics. Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> * fix(api): return 400 (not 500) on malformed JSON body (#6871) Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> * chore(ci): fix shared base-reds blocking PR queue (stryker registration + codex 0.144 test) - register tests/unit/cliproxyapi-model-mapping-dispatch.test.ts in stryker.conf.json tap.testFiles (gap from #6903) - update provider-models-route-codex.test.ts client_version 0.142.0 -> 0.144.0 (stale test from #6780 prod bump) * chore(quality): rebaseline cognitiveComplexity 885->890 (v3.8.47 merge-train burst) Owner-approved merge-burst reconciliation. cognitive-complexity does not run on PR->release fast-gates, so incidental growth across the 23-PR merge-ready batch accrued unmeasured (measured 890 on the combined merge-train tip vs 885 pristine). * chore(deps): bump github/codeql-action/analyze from 4.36.3 to 4.37.0 (#6831) * fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605) fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148). Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148). Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.) * deps: bump the development group across 1 directory with 6 updates (#6588) deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605). * fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620) fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.) * chore(deps): bump github/codeql-action/analyze from 4.36.3 to 4.37.0 Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.36.3 to 4.37.0. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](54f647b7e1...99df26d4f1) --- updated-dependencies: - dependency-name: github/codeql-action/analyze dependency-version: 4.37.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump github/codeql-action/init from 4.36.3 to 4.37.0 (#6832) * fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605) fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148). Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148). Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.) * deps: bump the development group across 1 directory with 6 updates (#6588) deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605). * fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620) fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.) * chore(deps): bump github/codeql-action/init from 4.36.3 to 4.37.0 Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.36.3 to 4.37.0. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](54f647b7e1...99df26d4f1) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.37.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat(provider): add OpenVecta AI inference gateway (#6833) * fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605) fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148). Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148). Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.) * deps: bump the development group across 1 directory with 6 updates (#6588) deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605). * fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620) fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.) * feat(provider): add OpenVecta AI inference gateway OpenVecta (https://openvecta.com/) is an OpenAI-compatible AI inference gateway hosting LLMs (GLM, Claude, DeepSeek, GPT OSS, Llama, Kimi, Nemotron...) plus text-embedding-* models behind a single Bearer key. Wiring (7 integration points): - src/shared/constants/providers/apikey/inference-hosts.ts: catalog entry - open-sse/config/providers/registry/openvecta/index.ts: registry w/ 9 seed LLMs - open-sse/config/providers/index.ts: wire into REGISTRY - src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts: live /v1/models URL - src/app/api/providers/[id]/models/discovery/providerSets.ts: NAMED_OPENAI_STYLE_PROVIDERS - public/providers/openvecta.svg: brand icon - tests/unit/openvecta-provider-registration.test.ts: regression guard (6 tests, all pass) No executor needed — buildOpenAiCompatibleRegistryEntry wires format=openai / executor=default / authType=apikey / authHeader=bearer. Live catalog discovery uses the existing NAMED_OPENAI_STYLE_PROVIDERS path (live /v1/models fetch + registry seed as offline fallback). Validation: - npm run typecheck:core clean - npm run typecheck:noimplicit:core 4 errors in unchanged files (combo.ts, cliRuntime.ts); 0 in new code - npm run lint clean - node --import tsx/esm --test tests/unit/openvecta-provider-registration.test.ts 6/6 pass - sibling tests/unit/openai-style-providers-4239-4155-3841.test.ts 18/18 pass (no regression) * chore(merge): drop unrelated main-drift from PR fork + fix count/golden drift The fork branch predated main's electron 42→43 bump (#6605) and several other package.json/lockfile churn; those files are unrelated to the OpenVecta provider addition and were reintroducing an older/stale state (version 3.8.46, electron 42, older bun/eslint-config-next) that broke the Electron Package Smoke check. Restored package.json, package-lock.json, electron/package.json, electron/package-lock.json, and scripts/build/prepare-electron-standalone.mjs to match origin/release/v3.8.47. Also updates the two provider-count assertions (166->167) and regenerates the translate-path golden snapshot to account for the new openvecta entry. Co-authored-by: hajilok <120608486+hajilok@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> * fix(sse): combo model lockout honors parsed upstream quota reset (#6863) (#6866) * fix(sse): combo model lockout honors parsed upstream quota reset (#6863) The combo failure path recorded model lockouts from checkFallbackError's cooldownMs only, discarding quotaResetHintMs — the ungated channel that carries a parsed upstream quota reset (e.g. Antigravity 429 "Resets in 92h27m28s"). With OAuth profiles defaulting useUpstreamRetryHints=false, the lockout fell back to the base cooldown (seconds), so quota-dead accounts were re-walked serially by every combo request for days (measured 122s per request, 494s worst case in #6863). Thread max(cooldownMs, quotaResetHintMs) into selectLockoutCooldownMs at both combo lockout sites, mirroring the single-model path pattern in src/sse/services/auth.ts (v3.8.43). All three resilience fences are preserved: useUpstreamRetryHints still gates connection cooldowns, the hint only affects model-scope lockouts, and combo 429s remain non-persistent. TDD: tests/unit/combo-lockout-quota-reset-6863.test.ts fails on base (lockout 5000ms) and passes with the fix (~92.5h). * test(sse): tighten #6863 lockout assertion to parsed-reset bounds; prettier pass Assert remainingMs falls within (parsedResetMs - 5s, parsedResetMs] so a hardcoded long cooldown cannot satisfy the regression test. Also apply Prettier to both changed files (includes one pre-existing formatting fix in handleRoundRobinCombo picked up by --write). * fix(sse): align combo lockout hint selection with single-model path; register test in mutation gate Adopt review feedback: replace Math.max(cooldownMs, quotaResetHintMs) with the auth.ts pattern (usedUpstreamRetryHint ? cooldownMs : quotaResetHintMs) so a parsed reset SHORTER than the fallback cooldown wins too — e.g. the subscription-quota branch returns a 1h fallback while the body says "resets in 45m"; max() would over-lock by 15 minutes. Add a regression test for the short-reset case (fails against the max() variant) and register the new test file in stryker.conf.json tap.testFiles to satisfy check:mutation-test-coverage --strict. * chore(quality): register cliproxyapi dispatch test in mutation gate tests/unit/cliproxyapi-model-mapping-dispatch.test.ts landed on release/v3.8.47 via #6903 without a tap.testFiles entry, so check:mutation-test-coverage --strict fails on the branch tip and on every PR merge ref. Register it so the gate is green again. --------- Co-authored-by: judy459 <JUDYZHU459@outlook.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * feat(proxy): shorthand proxy formats + protocol header mode for bulk import (#6867) * fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605) fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148). Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148). Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.) * deps: bump the development group across 1 directory with 6 updates (#6588) deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605). * fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620) fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.) * feat(proxy): add shorthand formats + protocol header mode for bulk import Supports 6 new shorthand formats alongside the existing pipe-delimited parser: - ip:port - ip:port:user:pass - user:pass@ip:port - user:pass:ip:port - protocol://ip:port - protocol://user:pass@ip:port Protocol header mode: a bare protocol name (http/https/socks5) on its own line sets the default type for subsequent protocol-less shorthand lines. Explicit protocol:// prefix always takes precedence over the header default. Changes: - Rewrite parseBulkProxyImport.ts with parseShorthandLine helper - Use Record<string, true> for static lookup tables (VALID_PROXY_TYPES, VALID_PROXY_STATUSES) per project convention - Add looksLikeHost() heuristic to disambiguate 4-colon format (ip:port:user:pass vs user:pass:ip:port) - Update BULK_IMPORT_TEMPLATE in ProxyRegistryManager.tsx with full documentation and examples for all formats - Update en.json bulkImportDescription to list all supported formats - Add 30 unit tests covering every format, edge cases, and regressions for the existing pipe-delimited path --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> * fix(sse): stop combo path tripping whole-provider breaker on plain 429 (#6868) * fix(sse): stop combo path tripping whole-provider breaker on plain 429 The combo path recorded a whole-provider circuit-breaker failure for a plain rate-limit 429, opening the breaker after N consecutive 429s and blocking every account+model on that provider. This contradicts the single-model path and the documented RESILIENCE_GUIDE policy. - Single-model path uses PROVIDER_BREAKER_FAILURE_STATUSES = Set([408, 500, 502, 503, 504]) (src/sse/handlers/chat.ts:206) — 429 excluded. - Combo path gated shouldRecordProviderBreakerFailure on isProviderFailureCode (accountFallback.ts), whose PROVIDER_FAILURE_ERROR_CODES INCLUDES 429 for connection-cooldown scope — so a plain 429 wrongly tripped the whole-provider breaker. Fix scopes tightly: comboPredicates now tests a local PROVIDER_BREAKER_FAILURE_STATUSES set mirroring the single-model constant (429 excluded), instead of isProviderFailureCode. The shared isProviderFailureCode / PROVIDER_FAILURE_ERROR_CODES are deliberately left untouched — they drive connection-cooldown / model-lockout logic where 429 must still count. A genuine quota/token-limit terminal 429 is handled elsewhere; only the whole-provider breaker-recording gate changes. Adds tests/unit/combo-breaker-429.test.ts covering the 429 exclusion, the 408/5xx inclusion, and the sameProviderNext / skipProviderBreaker suppression paths. * test(quality): register combo-breaker-429.test.ts in stryker tap.testFiles Fast Quality Gates' mutation-coverage drift check flagged this PR's new covering test for comboPredicates.ts as unregistered. Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> --------- Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * fix(oauth): embed Trae OAuth client_id via resolvePublicCred (Hard Rule #11) (#6870) * fix(oauth): embed Trae OAuth client_id via resolvePublicCred (Hard Rule #11) * fix(quality): register tests/unit/trae-publiccred.test.ts in stryker tap.testFiles The mutation test-coverage gate (check:mutation-test-coverage --strict) flags new covering unit tests that mutate open-sse/utils/publicCreds.ts but aren't listed in stryker.conf.json's tap.testFiles, so their mutant kills wouldn't count. Register the new test file alphabetically. Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> --------- Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * fix(ci): publish electron-updater latest.yml manifests in release assets (#6766) (#6881) * fix(ci): publish electron-updater latest.yml manifests in release assets (#6766) * fix(ci): register new mutation-covering test + realign stale codex-cli version fixture - stryker.conf.json: add tests/unit/cliproxyapi-model-mapping-dispatch.test.ts to tap.testFiles so it counts toward mutation coverage for the newly-added comboContextCache.ts coverage (check:mutation-test-coverage --strict was failing). - tests/unit/provider-models-route-codex.test.ts: DEFAULT_CODEX_CLIENT_VERSION was bumped to 0.144.0 on release/v3.8.47 after this test's fixtures were written; realign the hardcoded 0.142.0 expectations to the current constant. * fix(ci): exclude check-test-masking.test.ts fixtures from self-referential tautology gate (#6634) (#6884) * fix(ci): exclude check-test-masking.test.ts fixtures from self-referential tautology gate (#6634) * fix(ci): extend test-masking self-fixture exclusion to sibling gate regression files (#6634) The #6634 fix added isSelfTestFixtureFile()/scanBareTautologies() exclusions that only matched check-test-masking.test.ts exactly. Its own new regression file check-test-masking-selfref-6634.test.ts also embeds tautology-pattern literals as fixtures/documentation, so the absolute-floor scanBareTautologies gate self-tripped a HARD failure on the PR's own file. Generalize the exclusion to the whole check-test-masking* self-test family and lock it with two regression tests. * fix(api): route error responses through sanitizeErrorMessage (Hard Rule #12) (#6886) * fix(api): route error responses through sanitizeErrorMessage (Hard Rule #12) 9 API routes returned raw String(error)/error.message directly in HTTP 500 bodies, leaking SQLite paths, SQL text and internal messages. Route all through sanitizeErrorMessage() per Hard Rule #12: - settings/compression (GET+PUT), settings/compression/mcp-accessibility (GET+PUT) - cache/entries (GET+POST), db/health (GET+POST), db-backups/exportAll - assess, combos/test, settings/notion, settings/obsidian Test: tests/unit/rule12-error-sanitization-sweep.test.ts asserts sanitized 500 bodies contain no absolute paths / stack tails. * test(stryker): register rule12 error-sanitization sweep in tap.testFiles The new tests/unit/rule12-error-sanitization-sweep.test.ts covers a mutated module, so it must be listed in stryker.conf.json tap.testFiles for the mutation-coverage gate (check-mutation-test-coverage.mjs --strict) to pass. Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> --------- Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * fix(routing): honor no-auth provider connection isActive in auto-combo pool (#6557) (#6889) * fix(providers): strip redundant node prefix on connId-addressed custom models (#6772) (#6890) * fix(providers): give v0-vercel-web its own alias so credentials are detected (#6343) (#6891) * fix(providers): give v0-vercel-web its own alias so credentials are detected (#6343) * test(6343): type casts to satisfy no-explicit-any gate * test(6343): register v0-web + cliproxyapi tests in stryker tap.testFiles The two unit tests added on this branch cover mutated modules (src/sse/services/auth.ts, comboContextCache.ts) but were missing from stryker.conf.json tap.testFiles, tripping check:mutation-test-coverage --strict. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * fix(cli): waitForServer must not report ready on bare TCP accept (#6800) (#6892) waitForServer() polled /api/monitoring/health but fell back to declaring the server ready once the port had merely accepted TCP connections for >= 3s, even if no HTTP response was ever received. On CPU-bound warmup (e.g. small VPS running Next.js standalone), the OS-level listener accepts TCP almost immediately while the request pipeline is still compiling, so the fallback fired within ~3-7s and the CLI printed 'OmniRoute is running!' 30-60s before any route actually answered. Classify each health poll into ready / fast-reject / hanging / not-listening: only a fast HTTP rejection (fetch error that is not a timeout, e.g. ECONNRESET before the route mounts) grants the original #2460 Windows-cold-start grace window. A request that times out with zero response (the reported #6800 symptom) resets the grace window instead of accumulating toward it. Regression tests: tests/unit/waitForServer-tcp-fallback-6800.test.mjs (new RED-then-GREEN probe from the bug analysis) and tests/unit/cli-waitForServer.test.mjs (existing suite realigned to the corrected contract, plus a new case for the hanging-socket scenario). * fix(providers): wire devin cloud-agent into provider validation and static models (#6142) (#6894) * fix(sse): de-flake timing-sensitive combo cooldown/breaker tests (#6803) (#6897) * fix(sse): de-flake timing-sensitive combo cooldown/breaker tests (#6803) Extracts 3 wall-clock-sensitive assertions (combo-quota-share cooldown ceiling x2, circuit-breaker HALF_OPEN race) into tests/unit/serial/ (--test-concurrency=1, the repo's established remedy for this class of test) and widens their margins, since a starved CI-runner event loop can blow even a serialized test's timing window. Also adds an explicit 30s vitest timeout to the MCP audit shutdown test, which had no override and inherited vitest's 5000ms default. Regression proof: reproduced RED locally under real devbox CPU contention (2644ms/1796ms elapsed vs the old 1500ms ceiling, exactly the reported failure mode); confirmed GREEN after the fix under the same contention. * fix(quality): register new serial timing tests + prune stale any-suppression count - stryker.conf.json: add tests/unit/serial/combo-quota-share-cooldown-wait-timing.test.ts and tests/unit/serial/combo-strategy-fallbacks-half-open-timing.test.ts to tap.testFiles so their mutant kills count for accountFallback.ts and circuitBreaker.ts (PR #6897 added these files but didn't register them). - eslint-suppressions.json: combo-strategy-fallbacks.test.ts's no-explicit-any suppression count was stale (35) after this PR trimmed 2 any-usages out of the file when extracting the half-open timing test; corrected to 33. Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com> * fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401) (#6898) archiveLegacyRequestLogs() swept the entire DATA_DIR/logs directory as a single "legacy" target and recursively deleted it after zipping. Since PR #6234 moved the default app-log path to DATA_DIR/logs/application, the migration was deleting the live file logger's own directory on every boot until its marker file existed (#6799). Separately, yazl's addFile() does an internal stat-then-stream read; if a target file grows between those two steps (e.g. an actively-written log), yazl emits "error" directly on the ZipFile instance. That event had no listener, so Node re-threw it as an uncaughtException that crashed the whole process at startup (#6401) — misdiagnosed upstream as Turbopack/Windows chunk corruption because the stack trace pointed into a bundled chunk. Fix: - listArchiveTargets() now enumerates DATA_DIR/logs entries individually and skips the live app-logger directory (resolved via logEnv.getAppLogFilePath()), so the shared parent directory is never deleted wholesale. - createLegacyArchive() wires a zipFile.on("error", ...) handler so a stat/stream race rejects the promise (caught by the existing try/catch) instead of escaping as an uncaughtException. Regression test: tests/unit/usage-migrations-legacy-archive-safety.test.ts (RED on unfixed code, GREEN after fix). Updated tests/unit/request-log-migration.test.ts to the corrected contract — DATA_DIR/logs itself now survives the archive sweep. Gates run clean: file-size, complexity, cognitive-complexity, changelog-integrity, typecheck:core, eslint (suppressions), and the existing usage-migrations/request-log-migration unit suites. * fix(cli): ship head-response-guard.cjs in the standalone bundle (#6908) * fix(cli): ship head-response-guard.cjs in the standalone bundle server-ws.mjs imports ./head-response-guard.cjs, but assembleStandalone had no EXTRA_MODULE_ENTRIES entry for it, so every build:release bundle crashed at boot with ERR_MODULE_NOT_FOUND (found deployingd1d75fdbfto the VPS on 2026-07-11). Adds the missing entry plus a regression test that derives the required sidecar list from server-ws.mjs's own relative imports, guarding the whole class of missing-sidecar bugs. * docs(changelog): fragment for #6908 * fix(responses): escape literal control chars in tool call JSON; emit … (#6786) * fix(responses): escape literal control chars in tool call JSON; emit status=failed on upstream error #6785 Two bugfixes in the Responses API translator: 1. escapeJsonStringValues() sanitizes tool call arguments containing literal 0x0A/0x0D/0x09 bytes (emitted by Gemma4 models) into valid JSON \n/\r/\t escapes, preventing SSE framing corruption. Only escapes inside JSON string contexts — already-escaped sequences and structural JSON pass through unchanged. 2. sendCompleted() checks state.upstreamError and emits status="failed" with error.code + error.message instead of silently hardcoding status="completed" + error=null, so mid-stream errors (e.g. Gemini 503 after partial content) are properly surfaced to the client. 3. stream.ts: calls translateResponse(null,...) before controller.error() so the translator can emit close events (reasoning item done, response.completed) before the stream is terminated. * test(boundary): fix ESLint no-explicit-any warnings and quality gates Green the PR against release/v3.8.47 quality gates without weakening tests: - Replace @typescript-eslint/no-explicit-any in the new boundary/gemma4 tests with proper interfaces (ResponseBody, ToolDef, ToolArgs, SseEvent item accessors) — fixes the "No new ESLint warnings" gate. - Split tests/unit/translator-resp-openai-responses.test.ts (1079 LOC) by extracting the round-trip suite into a sibling file so both stay under the 800-line test cap — fixes check:file-size. - Rename the 5 live boundary tests to *.live.test.ts, gate them behind RUN_BOUNDARY_LIVE=1, add a test:boundary:live npm script and register the glob in check-test-discovery COLLECTORS — fixes check:test-discovery (they hit a live remote and must never run unopted in CI). Co-authored-by: Markus Hartung <mail@hartmark.se> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> * fix(providers): route AgentRouter key validation through CC wire image (#6377) (#6882) * fix(providers): route AgentRouter key validation through CC wire image (#6377) * test(6377): type fetch mock to satisfy no-explicit-any gate * fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) (#6887) * fix(providers): scope nvidia NIM 404s to the single failing model (#6773) (#6888) * fix(providers): scope nvidia NIM 404s to the single failing model (#6773) The nvidia registry entry multiplexes 17 models from 9 different upstream vendors (z-ai/, minimaxai/, deepseek-ai/, qwen/, mistralai/, stepfun-ai/, moonshotai/, openai/, nvidia/) behind one connection, but was missing passthroughModels: true — unlike 34 other multi-model registries (modelscope, synthetic, kilo-gateway, etc). Without it, hasPerModelQuota returns false for nvidia, so a 404 on a single stale/renamed model falls through checkFallbackError's generic catch-all as a connection-wide cooldown instead of being scoped to just that model, poisoning all 17 nvidia models for the cooldown window. Add passthroughModels: true to the nvidia registry entry so 404/429s on one model lock out only that model. Regression test: tests/unit/nvidia-passthrough-models-6773.test.ts * fix(quality): register nvidia passthrough test in stryker tap.testFiles check-mutation-test-coverage.mjs --strict flagged tests/unit/nvidia-passthrough-models-6773.test.ts as an unregistered covering test for accountFallback.ts (Fast Quality Gates). Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com> * fix(usage): strict validation for xAI exact provider-reported cost (#6856) extractUsageFromResponse() and normalizeUsage() used Number(x) coercion for cost_in_usd_ticks, which silently turned null/"" into 0 -- accepted downstream as a valid $0 exact cost instead of falling back to the token-based estimate. Both call sites now require typeof === "number" && Number.isFinite && >= 0. Rebased onto current release/v3.8.47 tip (already carries #6711) and trimmed to just the incremental validation fix + 2 regression tests, replacing the stale-base diff that re-added the whole already-merged feature. Co-authored-by: KooshaPari <koosha@phenotype.io> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(sse): treat compression no-op as zero-savings, not inflation/silent-drop (#6883) A structural engine (ccr / session-dedup) that finds nothing to compress returns the body unchanged. That no-op was mishandled three ways — the code-level root cause of the #6465–#6493 "0% savings, no reason" symptom class: A. Inflation guard mislabelled a no-op as inflation. guardPipelineInflation used `compressedTokens >= originalTokens`, so an unchanged body (compressedTokens === originalTokens) tripped the guard, setting fallbackApplied=true and emitting a misleading "did not shrink; reverted to original" warning. Changed to strict `>` — only a strictly larger output is inflation; equality is a no-op. Genuine inflation still reverts. B. Disabled-engine skip was silent and asymmetric with the breaker skip. Both stacked loops (sync + async) skipped a registry-disabled engine with a bare `continue`, recording no validationWarning — while the sibling breaker-open branch does. Both loops now add `${engine}: skipped (engine disabled in registry)`, mirroring the breaker branch. C. No-op engine lost its identity in engineBreakdown. mergeStackStep early-returned on null stats, pushing no breakdown entry, so ensureEngineBreakdown synthesized a generic "stacked" 0% node. It now records a zero-savings entry keyed on the engine that actually ran, preserving identity. Tests: tests/unit/compression-noop-guard.test.ts covers A (equal-token no-op not inflated; strictly-larger still reverts), B (disabled skip surfaces a "disabled" warning), and C (no-op engine keeps its own id in the breakdown). Updated the existing inflation-guard test whose net-zero case encoded the old buggy behaviour, and switched its wire test to object-form pipeline steps so the intended engine runs. Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * chore(quality): freeze file-size for #6909 (localDb 805) + #6807 (translator test 1195) Owner-approved /merge-prs tail freeze. localDb.ts is re-export-only (Hard Rule #2); translator test grew from #6807's regression suite. Both frozen (shrink-only). * fix(sse): default reasoning summary for effort-only Responses requests (#6807) A Chat-Completions client can only express reasoning via the top-level reasoning_effort hint and has no way to request a reasoning summary. When that hint is promoted to the Responses API's reasoning.effort, the upstream returns an empty summary and downstream chat clients see no thinking stream (encrypted reasoning only). Default reasoning.summary "auto" plus include ["reasoning.encrypted_content"] on the effort-only path so the summary actually streams back to the chat client, mirroring the Codex executor's ensureCodexReasoningSummary. An explicit reasoning object from a Responses-shaped client is preserved untouched, and reasoning_effort "none" is left without a summary. Adds regression tests for the effort-only default, the none case, and keeps the existing explicit-reasoning-object behavior unchanged. Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * fix(proxy): relay repair + free-pool UX + relay awareness (#6909) * feat(proxy): relay repair + free-pool UX + relay awareness * fix(proxy): preserve existing notes fields on relay repair; fix cpu limit /1000 across all 5 container providers * feat(proxy): extract bulk-import and pool-modal hooks (#6625) * fix: prevent relay type normalization to http on PATCH Bug: updateProxyRegistrySchema inherited .default("http") from the base schema, causing PATCH to silently overwrite relay types (vercel/deno/cloudflare) with "http" when the client didn't send a type field. - Move .default('http') from proxyRegistryFieldsSchema to createProxyRegistrySchema (only applies to new proxies) - Strip undefined keys from validated changes before passing to updateProxy — .partial() leaves absent fields as undefined, which the spread merge in updateProxyRow would propagate to the DB - Add console.warn in extractRelayAuth when decrypt fails on a known-encrypted relayAuthEnc blob Closes #6905 * feat: replace Load More with page-number pagination in FreePoolTab - Adds page-number pagination controls with prev/next buttons - Shows per-page summary with total counts - Resets to page 1 on filter change via wrapper setters * fix(proxy): restore free-proxy sync-error tracking reverted by pagination commit commit2c8e79f13(page-number pagination for FreePoolTab) accidentally reverted the recordFreeProxySyncErrors/clearFreeProxySyncErrors/ getFreeProxySyncErrors functions and the search/sortBy list options that an earlier commit (6f9ce75f3) in this same PR had added to src/lib/db/freeProxies.ts and src/app/api/settings/free-proxies/route.ts. localDb.ts still re-exported the three sync-error functions, so every module that transitively imports it (which is most of the unit test suite, plus the Next.js build used by dast-smoke) failed at load time with "The requested module './db/freeProxies' does not provide an export named 'clearFreeProxySyncErrors'". Restores the reverted implementation (verbatim) and fixes the two new "requires management auth" tests, which asserted 401 for an unauthenticated request without configuring INITIAL_PASSWORD + requireLogin first — on a fresh DB with no password configured, isAuthRequired() treats a loopback request as the pre-setup bootstrap path and allows it through, so the assertion needs the same password/requireLogin setup already used by tests/unit/api/settings-audit.test.ts. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> * fix(proxy): trim unrelated scope from relay-repair/free-pool PR Remove two subsystems that are unrelated to relay repair and the free-pool UX and were never wired into the app: - open-sse/services/combo/capabilityRequirements.ts and CapabilityRequirementsEditor.tsx: a combo capability-filtering feature never imported by combo.ts or combos/page.tsx, with no test coverage. - src/lib/skills/containerProvider.ts CPU-limit rescale: an untested change to sandbox resource limits, unrelated to the proxy/relay subsystem this PR targets. Restored to the release baseline. Also renumber the free_proxy_sync_errors migration 121 -> 122 to avoid colliding with #6855, which independently claims 121 on the same release branch. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(security): scrub hardcoded live-instance creds from boundary tests (#6786) The 3 tests/boundary/*.live.test.ts files merged via #6786 hardcoded a real Bearer API key, an auth_token JWT cookie, and a live instance URL. Replaced with env reads (OMNIROUTE_TEST_BASE/BEARER/COOKIE), preserving the RUN_BOUNDARY_LIVE gate. The leaked key/cookie must still be revoked/rotated on the affected instance and purged from history separately (operator action). * feat(compression): update vendored GCF (Headroom) codec to spec v3.2 — nested flattening (#6838) * feat(compression): update vendored GCF (Headroom) codec to spec v3.2 (nested flattening) Homogeneous arrays whose rows carry nested objects/arrays now tabularize via GCF v3.2 `>`-path flattening instead of a low-yield per-row fallback, so nested MCP tool-result rows (meta:{...}, tags:[...]) compact like flat rows. Round-trip stays lossless (order-insensitive deepEqual). Re-vendored from current gcf-typescript into the Headroom generic-profile codec (open-sse/services/compression/engines/headroom/gcf/); still zero runtime deps, MIT, SPDX-marked, generic-profile only. Also folds in two upstream round-trip-safety fixes: the [N]: inline-array quoting fix and canonical decimal formatting. Regression guard: tests/unit/compression/headroom-smartcrusher.test.ts gains a deep-nested case (two-level object + array-of-objects) asserting the v3.2 flatten paths and order-insensitive round-trip. Vendored-code baseline bumps (complexity 2053->2055, cognitive 885->888, decode_generic no-explicit-any 18->22) each carry an inline _rebaseline_2026_07_10_gcf_v3_2 justification noting the growth is the vendored surface, not new project code. * chore(changelog): add fragment for headroom GCF v3.2 nested flattening (#6838) * docs(readme): note Headroom handles nested arrays (GCF v3.2) in the engine-stack table * docs(readme): note Headroom handles nested arrays (GCF v3.2) in the engine-stack table * fix(compression): harden vendored GCF decoder against prototype pollution The v3.2 flatten/unflatten paths (and the pre-existing inline-object parser) built decoded objects with bracket assignment and `key in obj` membership, so a hostile or unusual payload could pollute Object.prototype via a `__proto__` path segment, and any key shadowing an Object.prototype member (`toString`, `constructor`) was misparsed or wrongly flagged duplicate. - Encoder (`analyzeFlattenable`): builds the shape map with `Object.create(null)` and refuses to flatten objects carrying `__proto__`/`constructor`/`prototype` keys (they round-trip whole instead). - Decoder: `unflattenPaths` drops any path with an unsafe segment; a shared `safeAssign` writes a literal `__proto__` key as an own data property (JSON.parse semantics) instead of reassigning the prototype, used at every object-build site; `checkDup` and orphan-merge use `hasOwnProperty` so built-in-named keys are not spuriously treated as duplicates. Also a losslessness fix: objects with keys named `toString`/`constructor`/ `valueOf` now round-trip. Regression guard: prototype-pollution + built-in-key cases in tests/unit/compression/headroom-smartcrusher.test.ts. Prototype pollution is JS/TS-specific; the Go/Python/Rust/Swift/Kotlin SDKs use native maps and are unaffected. * fix(compression): apply GCF decoder review hardening (hasOwnProperty sweep, unflatten null-guard, strict count) Addresses the second-round review on the vendored codec: - Replace every `key in obj` membership test with `Object.prototype.hasOwnProperty.call(...)` across generic.ts (flatten shape analysis, key-chain resolution, inline-schema/shared-array helpers, row encode) so inherited names (`toString`/`constructor`) never match the prototype chain, and remove a redundant `obj` re-declaration in the ">" field attachment loop. - `unflattenPaths` guards each intermediate segment: a missing OR non-object slot is replaced with a fresh object before traversal, so malformed/hostile input can no longer dereference a primitive and crash. - Use the strict `parseCount` helper (not `parseInt`) for the shared-schema count so malformed counts fail the mismatch check instead of coercing. The decoder grew past the 800-line file-size cap; frozen at 880 in file-size-baseline.json with a justification (vendored file kept faithful to upstream gcf-typescript for clean re-vendoring). Verified: prototype-pollution + hostile-input + built-in-key round-trip probes, 54/54 compression tests, typecheck, lint, cyclomatic/cognitive baselines unchanged, compression-budget. * fix(compression): do not flatten a nested object that is null in any row (losslessness) analyzeFlattenable skipped null values during shape analysis, so a field that was an object in some rows and null in others was still flattened. On decode, the null row's leaves resolved as absent ("~") and unflattened to a missing key instead of null, silently dropping the value (e.g. {meta:{owner:null}} decoded to {}). analyzeFlattenable now bails (returns null) when the field is null in any row, routing it through the lossless whole-object attachment path. Applies at every nesting depth via the existing recursion. Regression guard: null nested-object cases in tests/unit/compression/headroom-smartcrusher.test.ts. * fix(compression): narrow the null-nested flatten bail to intermediate nulls only The previous fix bailed flattening whenever a nested field was null in any row. That is correct but over-broad: a top-level null round-trips losslessly through flattening (it emits "-" and reconstructs via the all-null rule). Only a null at an intermediate nesting level loses data (its leaves encode as absent "~" and unflatten to a missing key). Bail only when parentPath is non-empty, so top-level nulls keep flattening (compression preserved) while intermediate nulls fall back to the lossless attachment path. Matches GCF conformance fixtures 004/013. --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * feat(providers): add GPT-5.6 model family (#6862) * feat(providers): add GPT-5.6 model family * fix(chatgpt-web): resume temporary chat handoffs * fix(codex): auto-merge discovery, filter denylist, revalidate on lifecycle Restore live/GitHub auto-merge for Codex catalogs, drop models via explicit denylist (GPT-5.4 family), and run scrub+live re-sync once on first-start, app upgrade, or setup completion. Success log: kill deprecated models complete. * fix(codex): preserve live catalog reconciliation Expose remote-only Codex models without dropping user custom entries, and complete lifecycle revalidation only after a successful internal sync. Keep credentialed self-fetches pinned to the active dashboard listener. --------- Co-authored-by: backryun <backryun@daonlab.local> * fix(release): read changelog.d fragments in list-uncovered-commits (#6857) (#6878) * fix(release): count changelog.d fragment refs in list-uncovered-commits (#6857) Since fragments-first (#6783), a merged PR's changelog entry usually lives in changelog.d/{features,fixes,maintenance}/<PR>-<slug>.md and is only folded into CHANGELOG.md at release time. list-uncovered-commits.mjs scanned only CHANGELOG.md, so every fragment-covered commit was reported as an uncovered gap (some fragments — e.g. 6708, 6709 — carry no #N in the body, only in the filename). Add fragment-aware ref collection: fragmentFilenameRef() reads the leading <N>- of a fragment filename, fragmentRefs() unions filename PR numbers with every #N in the body, and collectChangelogRefs() unions the CHANGELOG scan window with the fragment refs. main() now reads changelog.d via readChangelogFragments() and feeds it into the union. On release/v3.8.47 tip this moves 44 commits from uncovered to covered (215/341 vs the prior 171/341) without changing the covered/uncovered classification logic. * chore(release): add changelog.d fragment for #6878 Housekeeping item requested in review: the PR fixing changelog-fragment coverage tracking (#6857) did not itself have a changelog.d fragment. --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * [trim] feat(combo): add context requirements config for target filtering (#6907) * feat(combo): add context requirements config for target filtering Add contextRequirements config field to combo runtime config: - minContextWindow: filter models below threshold (0-10M tokens) - preferLargeContext: sort targets by context size descending - contextFilterMode: 'strict' excludes unknown limits, 'lenient' includes them Implementation: - Added Zod schema validation in combo.ts - Created contextRequirements.ts module with applyContextRequirements() - Integrated filtering after filterTargetsByRequestCompatibility() - Full test coverage with unit + integration tests Tests: 17/17 pass (combo-context-requirements.test.ts + integration) * feat(combo): add ContextRequirementsEditor UI component Add standalone React component for editing context requirements config: - Slider for minContextWindow (0 to 1M tokens) with presets - Toggle for preferLargeContext sorting - Radio group for contextFilterMode (strict/lenient) - Tooltips explaining each option - Active filters summary display Component features: - Shadcn UI components (Card, Slider, Switch, RadioGroup) - Preset buttons for common context sizes (8K, 32K, 128K, 1M) - Conditional display of filter mode when minContextWindow > 0 - Clear visual feedback of active filters Integration: Import and use in combo config form where other config fields like fusionTuning and judgeModel are edited. Pass combo.config.contextRequirements as value prop and update on onChange. Example usage: <ContextRequirementsEditor value={config.contextRequirements} onChange={(val) => updateConfig({ contextRequirements: val })} /> UI matches existing combo config editor patterns. * feat(combo): wire ContextRequirementsEditor into combo config form Adds context requirements section to combo edit page (strategy section), matching existing ResponseValidation pattern. Placed after response validation block, before agent features. * docs(combo): add context requirements feature documentation Covers: config schema, behavior, use cases, UI integration, troubleshooting, and test instructions. * fix(combo): pass provider+modelStr to getModelContextLimit for accurate context resolution Per gemini-code-assist review feedback: model names are not globally unique across providers. Passing both provider and modelStr ensures correct context limit resolution in applyContextRequirements(). * fix(combo): repair broken doc links and restore test:unit:fast flag - Point docs/combo-context-requirements.md 'Related' links at real docs (routing/AUTO-COMBO.md, architecture/RESILIENCE_GUIDE.md) — the three placeholder links (strategies.md/model-capabilities.md/fusion-tuning.md) did not exist and failed check:doc-links (Docs Gates fast-path). - Revert an out-of-scope package.json change to test:unit:fast that dropped --test-isolation=none; restore to match release/v3.8.47. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * test(combo): validate context requirements against the real Zod schema Point tests/unit/combo-context-requirements.test.ts at the real comboRuntimeConfigSchema export (src/shared/validation/schemas/combo.ts) instead of hand-duplicating the Zod schema inline, so the test catches schema drift. Also declare contextRequirements on DEFAULT_COMBO_CONFIG so resolveComboSetupConfig's inferred return type includes the key — combo.ts reads config.contextRequirements but the property was missing from the object typecheck:core infers types from, causing a build error. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> * fix(combos): remove dead ContextRequirementsEditor scaffolding (broken ui/card+label imports) The editor imported @/components/ui/card and @/components/ui/label which do not exist in the repo, breaking the Turbopack build. Removed the editor + its page.tsx usage + doc mention; the real fix (comboConfig contextRequirements default + test) is preserved. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * feat: add icons for 46 missing provider images (#6926) * feat: add icons for 46 missing provider images - Add SVG icons for 46 providers missing brand images - Add 3 LOBE aliases (bai, clinepass, copilot-m365-web) - Register all new SVGs in KNOWN_SVGS lookup New SVG icons cover: api-airforce, auggie, bluesminds, byteplus, bytez, charm-hyper, chipotle, chutes, crof, dgrid, digitalocean, dit, duckduckgo-web, factory, freeaiapikey, freemodel-dev, galadriel, gitlawb, gitlawb-gmi, hackclub, haiper, hcnsec, ideogram, kenari, leonardo, llm7, modelscope, nube, openadapter, orcarouter, pioneer, publicai, qiniu, requesty, sumopod, t3-web, theoldllm, tokenrouter, uncloseai, veoaifree-web, wafer, x5lab, yuanbao-web, zed-hosted, zenmux, zenmux-free * fix(icons): restore accidentally-deleted cohere alias in LOBE_PROVIDER_ALIASES The 46-icon addition dropped the existing `cohere: "Cohere"` entry; restore it alphabetically between codex-cloud and comfyui. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(sse): wire shared quota-fetch throttle into all provider fetchers (#6911) (#6963) OMNIROUTE_QUOTA_FETCH_MIN_INTERVAL_MS / quotaFetchThrottle.ts documents itself as used by 'the provider quota fetchers' (plural), but only codexQuotaFetcher.ts ever called throttleQuotaFetch(). N accounts on one IP for DeepSeek, Bailian, OpenCode, or Crof still burst simultaneously. Wire throttleQuotaFetch() into fetchDeepseekQuota, fetchBailianQuota (both the primary and China-region retry fetch sites), fetchOpencodeQuota, and fetchCrofUsage, placed after the existing cache short-circuit so cache hits stay unaffected (mirrors the codexQuotaFetcher.ts pattern). PROVIDER_LIMITS_SYNC_SPACING_MS / providerLimits.ts's OAuth vs non-OAuth split is left unchanged — that split is intentional by design and already regression-guarded by tests/unit/provider-limits-oauth-sequential-sync.test.ts. The generic usage.ts::getUsageForProvider dispatch path (github, glm, minimax, nanogpt, xai, etc.) is intentionally out of scope for this fix to avoid scope creep; ENVIRONMENT.md now documents the actual post-fix coverage instead of the prior overclaim. * fix(sse): rename max_completion_tokens to max_tokens for volcengine/DeepSeek (#6912) (#6964) * fix(sse): defer response.completed until trailing usage-only chunk (#6906) (#6965) Real OpenAI-compatible upstreams with stream_options.include_usage=true send finish_reason in one chunk (usage: null) and the actual token counts in a separate, trailing usage-only chunk (choices: [], usage: {...}). Both the live translator (openai-responses.ts) and the legacy transformer (responsesTransformer.ts) fired response.completed as soon as they saw finish_reason, so the trailing usage chunk's token counts were captured into state but never emitted -- Codex CLI and other /v1/responses consumers saw response.completed with no usage field (permanent 0% context-used). Both translators now defer response.completed via an awaitingTrailingUsage state flag when finish_reason arrives without usage already captured, and complete on the next usage-only chunk (or at stream end via the existing flush fallback) instead. Extracted the duplicated events/emit boilerplate into a new openai-responses/eventEmitter.ts leaf to keep the frozen openai-responses.ts file under its file-size baseline. Fixes 3 existing tests that encoded the old chunk ordering and adds a permanent regression test (tests/unit/responses-usage-trailing-6906.ts) covering both translators. * fix(sse): omit removed attachments field from Muse Spark Web request (#6935) (#6960) * fix(dashboard): label audio/embeddings/image compatible providers by kind on ProviderCard (#6936) (#6961) ProviderCard's compatibility badge used a binary apiType ternary (responses vs everything-else -> "Chat"), so audio-transcriptions, audio-speech, images-generations and embeddings compatible providers (e.g. a locally-hosted speaches TTS/STT server) were mislabeled as "Chat". Reuse the existing KIND_LABEL map (stt/tts/image/embedding) instead of adding new i18n keys. * fix(sse): classify LAN embeddings providers as no-auth (#6925) (#6962) Private/LAN embeddings provider_nodes (10.0.0.0/8, 192.168.0.0/16, 100.64.0.0/10 CGNAT) were excluded by a hand-rolled hostname filter that only matched localhost/127.0.0.1/172.16-31, forcing them through the apikey/bearer credential fallback and returning 401 for keyless local providers like a LAN Ollama instance. Reuse the shared isPrivateHost()/isCloudMetadataHost() classification from outboundUrlGuard.ts in both the dynamic-provider filter and the provider_node fallback branch, so any private host resolves to authType 'none' while cloud-metadata endpoints stay blocked. * fix(api): use local-first SSRF guard for LAN model-list discovery (#6939) (#6966) * fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) (#6959) cloakAntigravityToolPayload() injects decoy functionDeclarations (search_web, browser_subagent, read_url_content, generate_image) that mimic Antigravity's built-in server-side agent tools whenever any real tool is declared, but never set the companion toolConfig.includeServerSideToolInvocations flag a genuine Antigravity client sends alongside them. Google's Cloud Code backend (Gemini 3+) requires this opt-in whenever server-side built-in tool categories are combined with custom function declarations, causing every Antigravity tool-calling request to fail upstream. Set toolConfig.includeServerSideToolInvocations = true whenever decoy tools are injected. * fix(sse): escape backslash in ChatGPT-web citation link text (#6569) (#6944) * fix(sse): escape backslash in ChatGPT-web citation link text (#6569) markdownLinkText() escaped [ and ] but not the backslash itself, so a citation label ending in (or containing) a backslash produced a broken Markdown link — e.g. [Path C:\](url), where the trailing \ escapes the closing bracket and consumes the link. Escape the backslash first, then the brackets. Clears the CodeQL js/incomplete-sanitization alerts at open-sse/executors/chatgpt-web/citations.ts:52 (2 of the 9 new alerts on the v3.8.47 release PR). Regression guard: tests/unit/chatgpt-web-citations-escape.test.ts (trailing backslash, backslash-before-bracket, bracket-only, plain). * chore(changelog): add changelog.d fragment for #6944 Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: brick30llc-ctrl <admin@brick30.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(sse): flatten structured (array) content in Qwen Web executor (#6927) * fix(sse): flatten structured (array) content in Qwen Web executor foldMessages did String(m.content), turning OpenAI-style content-part arrays into the literal "[object Object]" prompt. Add contentToText() to extract the text parts. Reported on the support mesh. TDD: red->green regression test tests/unit/qwen-web-content-array-serialization.test.ts * docs(changelog): add fragment for #6927 * fix(stryker): register qwen-web content-array test in tap.testFiles Fast Quality Gates flagged the new coverage for open-sse/executors/qwen-web.ts as missing from stryker.conf.json's tap.testFiles list. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(db): add authType filter support to getProviderConnections (#6946) getProviderConnections ignored the authType query param, causing callers like tokenHealthCheck.ts and /api/token-health to fetch and decrypt every connection instead of only the OAuth ones they asked for. Add the missing auth_type WHERE clause and a regression test. Rebased to drop the unrelated 46-icon commit (duplicate of #6926) and the accidentally-committed tests/unit/authz/__stub_apiKeys.mjs runtime artifact; replaced with a real unit test asserting the authType filter excludes non-matching connections. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(tokenHealthCheck): case-sensitive provider comparisons break rotating/gh checks (#6947) ROTATING_REFRESH_PROVIDERS.has(conn.provider) fails for 'OpenAI' or 'Github' - the set is all lowercase. Same issue for the GitHub Copilot sub-token refresh guard. Both now normalize to lowercase before comparison, matching the established pattern from getHealthCheckSkipProviders() (line 201) and isGitHubAccessTokenOnlyConnection() (line 94). Replaces the whole-file regex assertion in oauth-providers-error-handling (which passed even on unfixed code) with two statement-scoped regression tests that fail against origin/release/v3.8.47's unfixed source and pass only once both call sites are normalized. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * perf: thread pre-fetched token to checkRateLimit avoiding re-query (#6930) * perf: thread pre-fetched token to checkRateLimit avoiding re-query getRelayTokenByHash already fetches the full RelayToken row. A few lines later checkRateLimit(token.id) does a second SELECT * FROM relay_tokens on a different predicate (id instead of token_hash). Change: - checkRateLimit accepts an optional existingToken parameter; when provided, skips the re-query entirely. - Both relay routes (chat completions + bifrost) pass the already- fetched token. - The function now uses RelayToken (camelCase) instead of RelayTokenRow (snake_case) when the token is passed in. PR-URL: fix-relay-thread-token * test(db): add regression coverage for checkRateLimit existingToken fast-path Adds node:test coverage for src/lib/db/relayProxies.ts::checkRateLimit proving the existingToken fast-path (pre-fetched RelayToken threaded in, no re-query) agrees with the legacy re-query path (no token passed), and that the per-minute cap is still enforced through the fast-path. Also adds a changelog.d fragment for the perf fix in9d4cd90e7. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix: eliminate redundant getApiKeyMetadata call in embeddings route (#6929) enforceApiKeyPolicy() already fetches the API key metadata and returns it as policy.apiKeyInfo. The old code at line 72 called getApiKeyMetadata a third time per request (third hash+DB query after isValidApiKey and enforceApiKeyPolicy's internal fetch). Change: use policy.apiKeyInfo directly instead of re-querying. Also removes the now-unused getApiKeyMetadata import. Adds a regression test exercising the dashboard-playground-key path (no bearer token, only enforceApiKeyPolicy's resolvePlaygroundTestKey fallback resolves the key) — the old apiKeyRaw-gated call always produced a null apiKeyMeta on that path, while policy.apiKeyInfo correctly carries it through to the downstream call log. Split out of the original PR: dropped the unrelated 46-provider-icon commit that had been bundled onto the same branch. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(sse): normalize assistant input_text to output_text in Codex Responses input (#6932) codex-cli replays assistant history with content parts typed as `input_text`, but the Responses API only accepts `output_text` (or `refusal`) on assistant turns — `input_text` is user-only. `normalizeCodexMessageContentPart` previously only rewrote parts literally typed `text`, leaving explicit `input_text` on assistant turns untouched, which the Codex/OpenAI backend rejects with a 400. Rewrite explicit `input_text` (and `text`) to `output_text` on assistant-role parts, dropping the assistant-only `annotations`, `logprobs`, and `obfuscation` fields. Mode-agnostic, applies to all Codex models. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(6813): fix thinking budget zero drop and default thinkingConfig injection (#6943) * fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (#6605) fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148). Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148). Supersedes #6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.) * deps: bump the development group across 1 directory with 6 updates (#6588) deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (#6605). * fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (#6620) fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes #6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from #6605 which passed the smoke. Not this diff.) * fix(6813): fix thinking budget zero drop and default thinkingConfig injection - Fix truthy check for budget_tokens to allow 0 - Stop injecting default thinkingConfig when no knobs present - Add tests covering all scenarios Related: #6813 --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat(sse): add connection backpressure for chat handler (#6590) Add checkConnectionCapacity guard with 429 + Retry-After in handleChat(). Introduce OMNI_MAX_CONCURRENT_CONNECTIONS env-bound cap, disabled (0) by default so existing deployments are unaffected until an operator opts in. Reconstructed from PR #6590, isolating only the backpressure change — the original branch also carried unrelated headroom/docker/perf work from the author's separate #6572 branch. Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(base): fix 2 mechanical release-tip base-reds (relayProbeStats re-export + OMNI_MAX_CONCURRENT_CONNECTIONS docs) * fix(sse): apply commentary-phase drop filter in TRANSLATE mode (#6952) (#6990) The #6199/#6561 commentary-phase filter (shouldDropResponsesCommentaryEvent) was wired only into createSSEStream's PASSTHROUGH branch. The TRANSLATE-mode loop (openai-responses upstream -> another client format, e.g. codex routes streaming into Claude Code) called translateResponse() on every raw chunk without checking phase, so internal commentary-phase scratchpad text leaked into the client-visible content channel as duplicate prose and narrated tool-call arguments. Extends the same stateful filter into TRANSLATE mode via a small factory (createTranslateCommentaryFilter) that owns its own item/index Sets, keeping the wiring in stream.ts (a frozen file) to a single guarded line. Fail->pass evidence: - tests/unit/repro-6952-commentary.test.ts against origin/HEAD (pre-fix): FAILED - "commentary-phase prose must not reach the translated client stream" - Same test against the fix: PASSED (2/2) * fix(combos): show embedding/rerank models and disambiguate duplicate names in builder options (#6975, #6957) (#6991) Removes the leftover chat-only isChatCapable gate from addModelOption() (#6975) and adds a name-disambiguation pass at the end of buildModelOptions() so distinct model ids sharing the same upstream display name fall back to their id (#6957). Both proven with TDD repro tests (RED->GREEN). * fix(sse): schema-aware optional tool-arg normalization for Codex routes (#6951) (#6992) stripEmptyOptionalToolArgs was allowlist-only (Read/Subagent) and only stripped empty-string/empty-array values, so Responses API strict mode (every property forced into `required`) could forward a forced non-empty value (e.g. Agent.isolation) or a schema-declared default value verbatim to the client. Add schema-aware drop-if-default and generalized drop-if-empty (any tool, gated on schema.required), and thread each tool's JSON Schema from the request's tools[] into the two streaming call sites (response.output_item.done handling). Closes #6951 * chore(base): fix release-tip base-reds — eslint severity revert (#6786 regression), migration gap 121, file-size freeze bumps * fix(test): align emergency fallback budget-exhaustion test with #6912 max_tokens normalization (#6967) The test asserted both max_tokens and max_completion_tokens=4096 on the nvidia/openai/gpt-oss-120b emergency fallback request. Commita34fb6b3e(#6912, merged into this release tip) added a symmetric normalization in chatCore.ts that renames/deletes the redundant max_completion_tokens field whenever the target provider supportsMaxTokens() (nvidia does), so only max_tokens reaches the upstream request. The old dual-field assertion is an outdated contract, not a regression. Align the test to the new intentional behavior while keeping the max_tokens=4096 cap assertion as the fallback-cap guard. * fix(test): deterministic openadapter live-catalog import repro (#6967) * chore(base): backfill #6909 i18n keys (en+pt-BR) and align gemini defaults test with #6943 (unit-full pre-flight) * chore(release): v3.8.47 pre-flight fixes — orphan test relocation (#6943), eslint suppression match, file-size/zizmor rebaselines * fix(quality): restore zizmorFindings ratchet object shape (value 169 + justification) * chore(quality): v3.8.47 cycle-close pct rebaselines (openapiCoverage 39.3->38, i18nUiCoverage 76.8->75.5) with justification * chore(release): open v3.8.48 development cycle * chore(release): bump v3.8.49 (development cycle version) * test(build): derive pack-artifact closures for all npm-shipped entrypoints (#7065 class) (#7081) The server-ws closure test hardcoded ONE wrapper and ONE import form. This generalizes it: every dist-root wrapper in EXTRA_MODULE_ENTRIES that ships in the npm channel has its local imports (static, dynamic import(), require()) required in both APP_STAGING_ALLOWED_EXACT_PATHS and PACK_ARTIFACT_REQUIRED_PATHS, and the bin/omniroute.mjs CLI boot path is closure-checked too — its direct imports bin/cli/data-dir.mjs and bin/cli/utils/storageKeyProvision.mjs were only covered by an allowlist PREFIX (absence from the tarball had no gate) and are now required paths. TDD: the bin closure test failed on those two before the policy fix. * docs(quality): codify retry policy per runner + release-level drift rule (WS5.4/WS5.5) (#7107) * chore(release): gate the sync-back push on release-green --quick (WS0.3) (#7083) The parallel-cycle sync-back (sync-next-cycle.mjs) is the one write path to the release branch with no CI gate — a red merged tree pushed there turns every PR in the cycle's queue red (G1). The script now runs validate-release-green --quick on the merged tree between the commit and the push; on HARD failure the commit stays local in the sync worktree for inspection. --skip-green-gate is the documented emergency hatch for reds verified pre-existing on the tip. TDD: greenGateArgs() flag contract + source guard asserting the gate call sits between main() and the push. * feat(ci): boot-smoke the packed npm tarball (check:pack-boot, #7065 class killer) (#7086) Three releases shipped a tarball that crashed on every boot (tls-options/3.8.41, head-response-guard #7040/#7065) because no gate ever EXECUTED the artifact. check:pack-boot packs the tree, installs the tarball into a clean prefix (postinstall runs for real), boots the installed CLI on a reserved port with an isolated DATA_DIR and polls /api/monitoring/health until it returns 200 with the packed version — failing loudly with the server's last output otherwise. Wired into the CI package-artifact job (reuses the dist/ the job already assembles) and into check:release-green --with-build (parallel slow wave). Live evidence: packed v3.8.49, installed and booted in 16.6s, health 200. * feat(ci): continuous release-green — on-push quick gate + 3x/day full sweep (WS5.1) (#7089) The v3.8.49 cycle started with what looked like a shared base-red because the tip had NO gate between pushes and the nightly (24h MTTD): the captain's sync-back is a direct push, and merged PR combinations are never validated together. nightly-release-green.yml becomes 'Release-Green (continuous)': - push to release/v* (code paths) → validate-release-green --quick (~5-8min) against exactly the pushed ref, with per-branch concurrency so merge storms collapse to the newest commit. The failure issue now names the offending push range (before..after, one merge per push in the normal queue — direct attribution without bisect). SHAs enter the shell via env (injection-safe); commit subjects go to the issue body through a file, never interpolated. - schedule → full --with-build --full-ci, now 3x/day (05:23/12:23/18:23 UTC). Workflow-only change (no production code); YAML parse validated. * feat(ci): duration-balanced E2E shards via LPT bin-packing (WS4.1) (#7090) Playwright --shard distributes by count (per file with fullyParallel:false), blind to duration — measured skew on the 9-shard matrix: 24m47s worst vs 1m47s best (14x), putting E2E on the CI critical path (~25min of the 33min gate). - scripts/quality/balance-e2e-shards.mjs: LPT greedy (heaviest first into the lightest shard) over config/quality/e2e-timings.json; deterministic (weight desc, filename tiebreak); new specs get the median weight; the CLI self-verifies the shard union equals the discovered spec list and exits non-zero on ANY inconsistency (missing timings, lost spec) so the CI step falls back to plain --shard — never fewer specs than before. - config/quality/e2e-timings.json: relative weights seeded from spec LOC (proxy); replace with real per-file durations from a full run when convenient (documented in _meta). LOC-seeded packing already lands at 742-761 per shard (1.03x skew) vs the alphabetical round-robin that produced 14x. - ci.yml test-e2e: balanced list per shard with logged assignment + fallback. TDD: 5 unit tests (LPT invariants, determinism, completeness, median fallback, seed-vs-specs drift guard). * feat(ci): TypeScript 7 native shadow for typecheck:core (WS4.2, advisory) (#7091) TS7 went GA 2026-07-08 (native Go compiler). Hybrid adoption is the officially documented pattern: the Compiler API only arrives in 7.1, so typescript-eslint, type-coverage and the Stryker checker must stay on typescript 6.x — only the pure type-check gate can move. This adds an ADVISORY shadow step to the fast-gates job running the SAME tsconfig.typecheck-core.json under TS7 via an isolated npx (deliberately NOT a dependency: an alias install could collide node_modules/.bin/tsc with 6.x and silently swap the blocking gate's binary). Live parity evidence (this tree): TS7 exit 0 / 0 errors vs TS6 exit 0 / 0 errors — identical verdicts. Local wall: 25s -> 19s (warm dev box; upstream reports 8-12x on cold/large runs — the shadow exists to measure OUR CI number). Promotion to blocking after ~1 week of parity, per the v3.8.49 plan. * feat(ci): hotfix fast-lane + tests-only E2E skip (WS3.1) (#7088) A hotfix with 3 fixes paid the full 33min gate 3x in v3.8.48 (owner: '6h to re-validate 3 fixes makes no sense'). Modeled on the Chromium/VS Code/Node emergency lanes — skip WAITING, never validation: - PRs labeled 'hotfix' (owner-applied; entry policy: production-broken only, previous green heavy-run linked as evidence, cherry-pick-only scope — documented in docs/ops/RELEASE_CHECKLIST.md) skip test-e2e (9 shards, the ~25min critical path), test-coverage, quality-gate and quality-extended. Build, unit shards, integration, vitest, lint bag, docs-sync, pack-artifact and the tarball boot-smoke still run: green in ~15min. - classify-pr-changes gains a testsOnly output: a diff entirely under tests/ with nothing in tests/e2e/ cannot change the served app, so the E2E matrix skips automatically (changing an e2e spec still runs e2e). TDD: 4 new classifier tests red->green; full-shape asserts aligned additively. * feat(ci): Windows leg for Electron prepare smoke (WS1.5) (#7113) The Electron rebuild/spawn path executed for the FIRST time on the release tag: the v3.8.48 Windows failure (npx.cmd spawned without shell) could only surface at release. The Electron Package Smoke job becomes a 2-leg matrix: ubuntu keeps the full pack + headless smoke; windows-latest runs prepare:bundle — the exact ABI rebuild + spawn-plan path that broke — on every release PR instead of tag day. tar extraction of the build artifact works on windows-latest (bsdtar). Workflow-only change; YAML parse validated. * chore(ci): gate hygiene — secrets baseline 0, semgrep drop, hadolint (WS6/D3 + WS1.7) (#7099) * chore(ci): gate hygiene — secrets baseline 0, semgrep metric drop, hadolint gate (WS6/D3 + WS1.7) - .gitleaks.toml: allowlist (with mandatory justification) for the 3 frozen generic-api-key false positives — latencyP50Ms/latencyP95Ms are metric FIELD NAMES and interleaved-thinking-2025-05-14 is Anthropic's PUBLIC beta header. quality-baseline secretFindings 3 -> 0: the ratchet is now zero-tolerance (verified: check:secrets --ratchet reports 0 findings, no regression). - quality-baseline: semgrepFindings removed — orphaned metric never wired to a blocking gate (semgrep.yml only echoes the count); CodeQL covers OWASP. - ci.yml lint job: hadolint on the Dockerfile (image pinned by digest, --failure-threshold error). Verified green against the current Dockerfile (5 pre-existing warnings visible, 0 errors). Also evaluated publint for the fast path (WS1.6) and REJECTED it with data: 1554 findings, ~all noise from the vendored dist/node_modules of the standalone package — wrong tool for this package shape; check:pack-boot is the real gate. * chore(ci): surgical baseline edit — preserve unicode formatting (was json.dump ensure_ascii noise) * feat(ci): Mergify merge queue + manual-train fallback runbook (WS3.4/WS3.2) (#7112) * feat(ci): Mergify merge queue for release branches + manual-train fallback runbook (WS3.4/WS3.2) D5 final decision (owner, 2026-07-13, post vendor research): Mergify OSS plan — free/unlimited for the public repo, with the two features the volume demands (85-100 active authors/month, 300+ PRs/week peaks, ONE merger): batching + automatic bisection of red batches (log2(N) vs N revalidations). Proven at larger scale by NixOS/nixpkgs. - .mergify.yml: queue for base ~= release/vX.Y.Z (the wildcard GitHub's native queue cannot do); entry ONLY via the owner-applied 'queue' label AFTER the pre-merge star gate (the label IS the approval — Mergify executes, never decides); merge_conditions '#check-failure=0' + '#check-pending=0' respect the path-filtered fast-gates; squash keeps one-commit-per-PR history; label auto-removed after merge. Freeze/cross-session guardrails documented in-file. - docs/ops/MERGE_TRAIN.md (WS3.2): the manual merge-train codified as the FALLBACK runbook (batch -> validate once -> bisect halves on red) + the tiering rationale (per-PR fast-gates, per-tip continuous release-green, per-release full matrix — nothing validated less, just per batch not per PR). - 'queue' label created in the repo. Config validated (YAML parse); Mergify's own config check runs on this PR. * fix(ci): mergify queue must not fail open — require the always-on Merge-integrity check as affirmative success * feat(ci): Codecov patch coverage (informational) + fix missing lcov reporter (WS5.6) (#7114) Two changes to the test-coverage job: - The CI c8 report step never emitted lcov (only text/json summaries), so the coverage-report artifact silently skipped coverage/lcov.info (if-no-files-found: warn) — the very file the Sonar job consumes. Adding --reporter=lcov makes the artifact real for both consumers. - codecov/codecov-action v5 (SHA-pinned) uploads the lcov after the summary, with codecov.yml keeping BOTH statuses informational during calibration (D7 decision: informative first, blocking only after ~2 weeks without false blocks). Philosophy: strict patch, lenient project — the global floor/ratchet already lives in c8 60% + quality-baseline.json; Codecov adds the diff view. Workflow+config-only change; YAML parse validated; CODECOV_TOKEN secret already created by the owner. * chore(ops): runner-box janitor + operations runbook (WS3.3) (#7115) * chore(ops): runner-box janitor script + operations runbook (WS3.3) Codifies what was manual discipline on the .113 self-hosted pool (two live incidents on the v3.8.47 release day): 30min cron sweeping stale runner temp/work dirs (>24h), disk-pressure alert at >=85% (SQLITE_FULL killed shards mid-run), and the proven 4-runner ceiling on the 16 GB box (8-wide OOM'd jobs; stopping a busy runner cancels its job — documented). Script smoke-tested live (disk 82%, 1 active runner, exit 0); bash -n clean. * docs(ops): reword error-code/bash-env mentions the fabricated-docs env detector misreads * fix(ops): harden janitor sweep — no symlink follow, -xdev, narrowed patterns (root-cron on world-writable /tmp) * fix(tests+providers): env-dependent tests exposed by GH-hosted runners (#6634 selfref shallow checkout + yuanbao live-network 401) (#7174) * fix(tests): #6634 selfref test tolerates shallow checkouts (fetch origin/main on demand, skip offline) * fix(providers): yuanbao cookie validation rejects foreign pairs locally (was a hidden live-network test dependency) * feat(release): post-publish verifier — clean-container install + boot (WS1.4) (#7109) * feat(release): post-publish verifier — clean-container install + boot (WS1.4) verify-published.mjs installs the PUBLISHED version from the public registry inside node:24-slim and boots it until /api/monitoring/health returns 200 with the expected version — validating the exact bytes users install, on a machine with no repo/devbox state. Version + knobs travel as docker env vars, never interpolated into the container script (Hard Rule #13); strict semver arg validation. Wired into the release Phase 4 monitoring playbook. Live evidence: omniroute@3.8.48 from the real registry installed and booted in a clean container — HTTP 200, version 3.8.48, exit 0. Tests: 4 pure-function guards (semver strictness incl. shell-hostile rejects, env-passing invariant, clean-image pin, health-poll source guard). * chore(quality): allowlist verify-published container env vars in env-doc-sync * feat(release): npm staged publishing + pre-publish boot-smoke (WS1.3) (#7092) * feat(release): npm staged publishing + pre-publish boot-smoke (WS1.3) v3.8.47 shipped an npm tarball that crashed on every boot and had to be deprecated — the publish path had no runtime gate and the owner's 2FA happened BEFORE any proof. Two changes to npm-publish.yml: - check:pack-boot runs right before any publish (dist/ is already assembled by build:cli in the same job) — a non-booting tarball now fails the workflow before anything reaches the registry. - npm publish becomes 'npm stage publish' (staged publishing, GA 2026-05-22, npm >= 11.15 ensured in-job): the exact bytes are parked on the registry but NOT installable until the owner runs 'npm stage approve <id>' with 2FA. The workflow summary prints the approve/verify/reject flow; RELEASE_CHECKLIST documents the owner flow, the one-time Trusted Publisher stage-only config, and the deprecate-first rollback playbook. publish_mode=direct (workflow_dispatch) is the emergency fallback to the legacy immediate publish. First real-registry exercise happens on the next release with the fallback one dispatch away (D2 decision, v3.8.49 plan). GitHub Packages secondary publish unchanged. YAML parse validated. * docs(release): reference upcoming verifier without file paths (docs-all strict) * fix(release): pin npm 11.15.0 in the staged-publish version guard (no @latest in the publish job) * feat(homolog): real-environment E2E homologation suite (npm run homolog) (#7133) * feat(homolog): scaffolding da suíte de homologação E2E (deps + npm run homolog) * feat(homolog): L0 avaliador de paridade de deploy (TDD) * feat(homolog): L1a ciclo de vida de API key efêmera (login admin -> create -> revoke) * feat(homolog): L1b suite httpYac de API (models, chat, auth de management, health) * feat(homolog): L1c checker SSE de streaming real (TDD no parser) * feat(homolog): L2 smoke de providers reais via promptfoo gerado do catálogo * feat(homolog): L4a Playwright homolog config + login storageState * feat(homolog): L4b smoke de todas as rotas do dashboard (descoberta via fs) * feat(homolog): L4c fluxo criar/revogar API key pela UI * fix(homolog): resiliencia real-environment — stream:false no smoke promptfoo, retry de socket keep-alive, key efemera com sufixo unico * feat(homolog): L5 orquestrador npm run homolog + relatorio CTRF unificado * docs(homolog): guia de operacao da suite + fragment de changelog + allowlist env-doc-sync * fix(homolog): paraleliza o sweep de rotas do dashboard (fullyParallel + 8 workers) * fix(homolog): isola outputs crus em homolog-report/raw para nao quebrar o ctrf merge * fix(homolog): outputDir absoluto do reporter CTRF da UI (path relativo escapava do worktree) * chore(quality): allowlist the 5 homolog-suite devDependencies (ctrf-io trio, httpyac, promptfoo) after registry verification * chore(quality): register the homolog Playwright suite as a test-discovery collector (run.mjs -> tests/homolog/ui) * fix(ci): raise dast-smoke timeout 12->25min (build alone eats up to 11min) (#7139) * fix(tests): vitest UI suite back to green (69 fails triaged — WS6.1) (#7127) test:vitest:ui was advisory/parked with 70 failing tests across 30 files (of 159 total). Triaged by grouping failures by root cause instead of fixing one-by-one: - 15 files (use-virtual-list, use-traffic-stream, use-system-proxy-exit-guard, use-session-recorder, use-resizable-panels, traffic-inspector-page, timing-i18n, stats-tab, session-recorder-bar, same-context-filter, historic-session-banner, conversation-tab, conversation-tab-separators, cli-tools-no-mitm-tab, agent-bridge-server-card-a11y) were authored against node:test but live under tests/unit/ui/*.test.tsx, which vitest.config.ts collects but test:unit's glob (only *.test.ts) never does — orphaned. Fixed by switching their describe/it/beforeEach imports to "vitest". - jsdom does not implement window.matchMedia, and several dashboard components read it via useTheme() (directly, or transitively through ProviderIcon). Added tests/_setup/vitestUiPolyfills.ts (wired into vitest.config.ts) with a minimal MediaQueryList polyfill — fixed providerCascadeNode, ProviderIcon-icon-url, CliAgentsPage, playground-studio, comboLiveStudio, memories-tab, home-topology-hidden, ProxyRegistryManager-tdz. - playground-build-tab.test.tsx (9 tests) and compressionHub*.test.tsx (2 tests) asserted against pre-redesign UI: BuildTab now sits behind a 3-step BuildWizard (mode picker -> configure -> run), and CompressionHub is a Phase-2 thin overview without the old master toggle/mode selector/pipeline list. Rewrote the build-tab test to drive the wizard, and removed the two compressionHub.test.tsx assertions already superseded by compressionHub-active-selector.test.tsx. compressionHub-context-editing.test.tsx asserted stale Portuguese copy against a component that deliberately uses literal English strings (documented hydration workaround) — aligned to the real text. - search-tools-compare-tab.test.tsx: the D22 4-provider cap documented in docs/frameworks/SEARCH_TOOLS_STUDIO.md was never implemented in CompareTab — fixed the component (disable extra toggles + cap selectAll + warning message) since the test was correct and the component was the bug. Also fixed an assertion looking for a <table> that never existed (the results panel is a div-based side-by-side layout). - CliAgentsPage.test.tsx: the agent-tool catalog grew from 6 to 8 (omp, letta added) since the test was written — updated the fixture and expected count. - memories-tab.test.tsx: a call-order-dependent fetch mock (mockResolvedValueOnce + fallback) broke once MemoriesTab started firing an immediate health check that raced its 300ms-debounced list fetch — switched to a URL-keyed mock like the rest of the file. - home-topology-hidden-4596.test.tsx: useLiveDashboard now runs an async handshake fetch before opening the WebSocket — stubbed fetch and awaited it. - same-context-filter.test.tsx: the filter branch moved from useTrafficStream.applyFilter into the extracted, reusable matchesTrafficFilter() helper — updated the source-grep target. - tests/unit/ui/provider-plan-config.test.tsx deleted: it tested ProviderPlanConfigClient, which tests/unit/quota-plans-route-retired.test.ts proves was deliberately retired (Plans screen removed). Result: test:vitest:ui 158/158 files, 870/870 tests passing (was 30 failed / 159, 70 failed / 743). test:vitest (MCP/autoCombo) still green at 28/28, 253/253. Not promoted to blocking in this PR per the task — the owner promotes after reviewing the green suite. * chore(ci): promote test:vitest:ui to blocking (suite green after #7127) (#7147) * fix: preserve relayAuth for pool-referenced relay proxies (#5716) (#7182) * fix(providers): reject chat requests for cloud-agent-only jules provider (#6699) (#7193) * fix(db): cap OOM probe-failure cycle in getDbInstance() (#6835) (#7186) When better-sqlite3/node:sqlite are unavailable and the sql.js WASM fallback OOMs while probing storage.sqlite, getDbInstance() rethrew an identical 'Out of memory while probing' error on every call, forever — unlike the generic-corruption probe-failure path (#6632), which correctly caps at 3 attempts via the restore-count cycle breaker. Because the OOM path never renames the file away (intentional — OOM is not corruption), the existing cap is structurally unreachable for this branch, so every independent background poller (BATCH, ProviderLimitsSync, HealthCheck, ModelSync) kept re-triggering the same failure with no terminal diagnostic, hanging the app forever. Adds an independent __omnirouteDbOomFailureCount cycle-breaker mirroring the existing threshold of 3, throwing a distinct terminal 'Aborting startup' diagnostic after repeated OOM failures instead of looping. Does not touch the rename/backup safety mechanism. Reported-by: xHmeyer, mostafa-binesh * fix: route zai-web (and other registry-entry web-cookie providers) connection-test cookie probe through the configured proxy (#7058) (#7192) * fix: restore mobile grid-cols-1 fallback on quota page card grid (#7072) (#7194) * fix: include proxyId when testing a saved registry proxy (#7080) (#7189) * fix: surface real claude-web error body for non-SSE 400s (#7134) (#7196) tlsFetchStreaming() streams the upstream response to a temp file via tls-client-node's streamOutputPath mode. For a non-SSE, non-2xx response the native binding resolves with an empty in-memory `body` field even though the real error bytes were already written to (and peeked from) the temp file, so genuine Claude 400/403/429/500 error details were silently discarded and replaced with "no response body". Fall back to a bounded read of the temp file when the resolved response's body is empty, and export tlsFetchStreaming for dependency-injected testing without --experimental-test-module-mocks. * fix(dashboard): agent bridge dns toggle uses POST, not PUT (#7157) (#7197) The dns toggle button called fetch(..., { method: "PUT" }) but src/app/api/tools/agent-bridge/agents/[id]/dns/route.ts only exports POST, so Next.js auto-returned 405 on every Start/Stop DNS click. Fixes the frontend caller to match the documented POST contract (docs/frameworks/AGENTBRIDGE.md:490) already covered by tests/unit/agent-bridge-dns-route-validation.test.ts. Adds a regression test asserting the fetch call uses method: POST. * fix(dashboard): implement missing handleToggleSource on Free Pool tab (#7161) (#7200) * fix: sanitize non-Latin1 chars in combo diagnostic headers (#6612) (#7190) * fix(providers): refresh OpenCode (oc) free-tier model catalog (#6998) (#7188) The oc registry entry (opencode.ai/zen/v1) hardcoded 6 free-tier model IDs (minimax-m3-free, minimax-m2.5-free, ling-2.6-1t-free, trinity-large-preview-free, nemotron-3-super-free, qwen3.6-plus-free) that were delisted upstream and now return 401 "Model X is not supported". Live upstream instead offers 4 different free models (mimo-v2.5-free, hy3-free, nemotron-3-ultra-free, north-mini-code-free) that were never added to our static catalog. Swap the 6 delisted IDs for the 4 currently-live ones, confirmed against https://opencode.ai/zen/v1/chat/completions on 2026-07-14. Updates two existing tests (minimax-m3-model-registry, provider-registry-qwen-vision) that asserted the now-delisted minimax-m3-free was present in the oc catalog — they now assert its absence, matching the corrected contract. * fix: honor combo-level proxy assignments from the registry (#7149) (#7201) * fix(providers): DuckDuckGo VQD 429 misclassified as 503 (#6996) (#7185) acquireVqdHeaders() discarded the upstream HTTP status of the /duckchat/v1/status call and collapsed every non-2xx response to {vqd4:null, vqdHash1:null}. execute() then always returned a hardcoded 503 when the token could not be acquired, regardless of whether DuckDuckGo actually returned 429 (rate limit), 403, or a genuine 5xx. This mattered beyond the confusing error message: per the resilience contract only 408/500/502/503/504 should trip the whole-provider circuit breaker, not 429. Mislabeling a real 429 as 503 caused the entire ddgw/* catalog to get knocked offline for the breaker reset window instead of a short cooldown. Now acquireVqdHeaders()/acquireAuthHeaders() thread the real status and Retry-After header through, and execute() surfaces a genuine 429 (with Retry-After) instead of the hardcoded 503; the 503 fallback is kept for non-429 failures and network errors. Regression test: tests/unit/duckduckgo-vqd-429-misclassification-6996.test.ts * fix: wire modelAliases fetch into HermesAgentToolCard (#7151) (#7195) * fix: stop duplicating text in Gemini Web streamed responses (#7163) (#7198) * fix: extend turbopack ignoreIssue suppression to compression module (#7051) (#7180) * fix: wire adaptive context-budget dial into settings schema and DB (#7005) (#7183) * fix: wire adaptive context-budget dial into settings schema and DB (#7005) * chore(db): re-export compressionContextBudget from localDb.ts per db-rules gate (#7005) * chore(db): keep localDb.ts line-neutral after compressionContextBudget re-export (#7005) * fix: recognize Ollama Cloud session usage-limit 429 as quota-exhausted (#7071) (#7181) * fix: recognize Ollama Cloud session usage-limit 429 as quota-exhausted (#7071) Ollama Cloud's 5-hour "session" usage-limit 429 body ("you (NAME) have reached your session usage limit...") was never recognized as quota-exhausted -- only the sibling "weekly usage limit" wording was fixed (#6638/#3709). Neither the generic QUOTA_PATTERNS list nor the dedicated weekly-quota classifier matched the session wording, so checkFallbackError() fell through to the generic ~3s rate-limit backoff instead of a long QUOTA_EXHAUSTED cooldown -- combo/LKGP routing cycled back to the "exhausted" account almost immediately instead of advancing to the next one. Adds isSessionUsageLimitText()/buildSessionQuotaFallback() to quotaTextCooldowns.ts, mirroring the weekly-quota pair, with a 5h cooldown matching Ollama Cloud's documented session window. Wired unconditionally into checkFallbackError() next to the weekly check so apikey-category providers like ollama-cloud are covered. * chore(test): register issue-7071-ollama-session-quota.test.ts in stryker tap.testFiles (#7071) * fix: stop opencode-go quota lookup defaulting to Z.AI endpoint (#7022) (#7187) * fix: stop opencode-go quota lookup defaulting to Z.AI endpoint (#7022) getOpenCodeGoUsage() defaulted OPENCODE_GO_QUOTA_URL to https://api.z.ai/api/monitor/usage/quota/limit, a Zhipu AI (Z.AI/GLM) endpoint unrelated to opencode.ai. Whenever a connection had no dashboard-scraping config (workspaceId/authCookie), the user's real OpenCode Go API key was sent as a Bearer token to that third-party host by default, with no operator opt-in. Remove the hardcoded default: the quota-by-API-key fetch now only runs when the operator explicitly sets OMNIROUTE_OPENCODE_GO_QUOTA_URL. With it unset (the default), getOpenCodeGoUsage() returns a descriptive message and makes zero outbound calls, since OpenCode Go has no public quota API. Also updates .env.example and both EN/zh-CN copies of docs/reference/ENVIRONMENT.md to drop the stale Z.AI default value and fix the stale open-sse/services/usage.ts source-file reference. Regression test: tests/unit/opencode-go-quota-no-zai.test.ts (RED on current code, GREEN after the fix). * fix: align opencode-go-usage tests with opt-in quota URL contract (#7022) The prior commit removed the hardcoded api.z.ai default from OPENCODE_GO_QUOTA_URL, making the quota-by-API-key path opt-in via OMNIROUTE_OPENCODE_GO_QUOTA_URL. Six pre-existing tests in opencode-go-usage.test.ts still asserted the old default-fetch behavior and the old Z.AI-specific error wording, so they broke. Set OMNIROUTE_OPENCODE_GO_QUOTA_URL before the module import (the value is read once at load time) to simulate an operator who opted in, and update the two error-message assertions to the new generic wording ("the configured OMNIROUTE_OPENCODE_GO_QUOTA_URL endpoint" instead of "the Z.AI quota API"). Each test still verifies exactly the same behavior it did before (invalid key, fetch failure, 200 with auth error in body, invalid JSON, quota shape) — only the opt-in setup and message wording changed. * fix: filter hidden custom models out of legacy combo model picker (#7156) (#7199) * fix: filter hidden custom models out of legacy combo model picker (#7156) * chore(test): move model-select-modal-hidden-models-7156 test into tests/unit/ui (collector coverage) (#7156) * fix(ci): run quality gates on Mergify merge-queue draft PRs (anchor check never ran, queue always dequeued) (#7202) * fix: add dashboard-scoped typecheck gate covering src/app/(dashboard) TSX (#7033) (#7203) typecheck:core (the only blocking CI typecheck gate) runs against a curated 27-file allowlist that excludes all src/app/(dashboard) TSX, and next.config.mjs sets typescript.ignoreBuildErrors: true so next build never type-checks it either. Orphaned-identifier regressions there (the exact class fixed in #6625/#6909) were invisible to CI. Adds tsconfig.typecheck-dashboard.json (extends tsconfig.json, scoped to src/app/(dashboard)/**/*.ts(x)) plus check:dashboard-typecheck, a gate script that runs tsc against it and diffs per-file/per-TS-code error counts against a frozen baseline (config/quality/dashboard-typecheck-baseline.json, 262 pre-existing errors), following the same stale-enforcement allowlist pattern as check-known-symbols. Only NEW errors beyond the baselined count fail the gate; wired as a new blocking step in ci.yml (lint job) and quality.yml (fast-gates). Regression test (tests/unit/build/check-dashboard-typecheck.test.ts, 8 tests) reproduces the #6625/#6909 orphaned-identifier bug class against the pure parseTscOutput/diffAgainstBaseline helpers. * fix: raise main server keepAliveTimeout/headersTimeout above Node's 5s default (#7003) (#7191) * fix: raise main server keepAliveTimeout/headersTimeout above Node's 5s default (#7003) JetBrains AI Assistant's pooled java.net.http.HttpClient reuses a keep-alive connection past Node's unconfigured 5_000ms keepAliveTimeout, hitting a socket the server already tore down and getting 0 response bytes back ("HTTP/1.1 header parser received no bytes"). Wire a new getMainServerTimeoutConfig() (mirroring apiBridgeServer's pattern) into run-next.mjs so the main dashboard/API server raises keepAliveTimeout to 65s and headersTimeout to 66s by default, both env-overridable. * fix: wire main-server keepAlive timeouts into standalone/production server path (#7003) getMainServerTimeoutConfig() was only wired into scripts/dev/run-next.mjs, the dev-only entry point for `npm run dev`/`npm start`. The server real end users run — `omniroute serve` (npm-installed CLI), Docker, and Electron — spawns the standalone Next build's server.js via run-standalone.mjs, which prefers server-ws.mjs (built verbatim from scripts/dev/standalone-server-ws.mjs by assembleStandalone.mjs) over the bare server.js precisely because it wraps http.createServer with production behavior the bare server lacks. That wrapper never configured keepAliveTimeout/headersTimeout, so the JetBrains AI Assistant reconnect bug this issue reports still hit the production entry point after the first pass of this fix. Wire the same helper into the wrapped server object there too. * feat(ci): Trunk Flaky Tests uploads for vitest + Playwright E2E (WS5.2/5.3) (#7175) * feat(ci): Trunk Flaky Tests upload on the fast-path vitest job (per-PR volume) (#7205) * Add cliproxy provider exposure controls and manifest injection * fix(ui,services): expose cliproxy provider in manifest when absent upstream * test(services): assert service providers expose models via v1 provider models API * refactor(services): centralize service-provider backend identifiers * test(services): cover service backend helper primitives * refactor(services): share embedded service manifest metadata * test(services): lock manifest metadata for synthesized backend providers * chore(release): script the 0a.0b PR re-home with a verified read-back (#7312) The parallel-cycle model hands the frozen release/vX to the captain and cuts release/vX+1 for everyone else. Phase 0a.0b step 3 then re-homes every open PR onto the new cycle — today as a hand-run loop of gh pr edit --base. Three things make that loop unreliable at exactly the moment it matters: 1. gh pr edit --base FAILS SILENTLY (v3.8.42). It exits 0 and leaves the base untouched, so every edit needs a gh pr view --json baseRefName read-back. A human mid-release skips that. 2. gh pr list caps at 30 results by default. A loop written without --limit re-homes the first 30 of 148 and reports success. 3. Volume: the v3.8.49 freeze had 148 open PRs — roughly 450 API calls across edit, verify and comment. The script does the read-back on every PR, uses --limit 300, is idempotent (a PR already on the next base is skipped, so a resumed release re-runs safely), refuses to start when the next branch does not exist yet, and exits non-zero listing any PR whose retarget did not take. It also prints the reminder that it cannot solve the other half: PRs opened AFTER it runs. Those need the repo default_branch pointed at the live cycle — contributors open PRs against the default branch, and while that stays on main they never target a release branch at all (6 such PRs on 2026-07-15). classify() is pure and unit-tested: retarget open and draft PRs on the frozen branch; never touch main (the release PR's own lane), an older shipped release, or a PR already re-homed. Refs #7307 * fix(build): packed tarball boot crash — server-ws timeout import escaped the package (#7065 class) (#7308) * fix(build): server-ws timeout helper as shipped sibling — ../../src import crashed every packed boot (#7065 class) * test(build): align pack-artifact-policy fixture with the new dist/main-server-timeouts.mjs required path * fix(skills): register cli-skill-collector in the agent-skills catalog (Integration 2/2 base-red) (#7310) * fix(skills): register cli-skill-collector in the agent-skills catalog (#6294 shipped the dir only) * chore(skills): regenerate cli-skill-collector SKILL.md via the generator, preserving the #6294 authored workflow in the custom block * fix(skills): derive coverage totals from the id lists + align remaining count assertions (45 catalog / 21 cli) * fix(skills): SkillCoverage totals are number, not stale literals * test: update stale ninerouter version fixture and prune stale suppressions * chore(ci): make the Electron Windows leg advisory with bash stderr capture (first-run failure diagnosis) (#7340) * fix(ci): Coverage job timeout 10->20min (lcov reporter pushed it past the old cap) (#7342) * test(ci): make #6634 selfref guard hermetic — read file from disk, no git ref (#7327) check-test-masking-selfref-6634.test.ts did git I/O inside a unit test (`git show origin/main:<file>`), the single most common red across today's babysit sweep — GitHub-hosted runners use shallow/single-ref checkouts with no origin/main, so the show fails with "fatal: invalid object name". The prior hotfix (2e42b8efc, #7174) wrapped it in try/catch + on-demand fetch + t.skip() on failure, but t.skip() itself trips the PR Test Policy weakened-assert gate (confirmed today on #7300), and origin/main was the wrong ref anyway — PRs target release/v3.8.49, not main. Ported the hermetic version proven on PR #7300 (@growab): read the real current source of check-test-masking.test.ts from disk instead of diffing against a git ref, and use an empty-string base (baseTaut/baseExtTaut = 0) instead of the pre-#6404 git snapshot — this maximizes headTaut - baseTaut, the strictest input for the exclusion under test, so the guard is exercised at least as hard as before. No git ref, no skip, no CI-shape dependency. Verified both directions locally: - SELF_TEST_FIXTURE_RE neutralized in check-test-masking.mjs -> test FAILS (10 new bare tautologies + 28 new extended tautologies reported) - restored -> test PASSES, and the full check-test-masking.test.ts suite (55 tests) stays green, confirming the #6634 self-referential-fixture regression this guard exists for is still covered. Co-authored-by: growab <nekron@icloud.com> * chore(quality): tighten the coverage ratchet to the CI's real numbers (#7326) The Quality Ratchet has been red on main, and not for a regression — the report says 'OK (57 métricas, 11 melhoraram)'. It fails the --require-tighten step: ✗ coverage.branches: melhorou de 73 para 78.1 (delta 5.1000 > slack 5) — rode 'npm run quality:ratchet -- --update' e commite o baseline apertado The gate was asking for this in plain text. The baseline's own note names the same trigger: 'Apertar via quality:ratchet -- --update a partir do 1o run de coverage mergeada do CI que popule essas chaves.' Values are the CI's, not a local run. The baseline warns that a local test:coverage measures ~68% against the CI's ~76.5% — tightening to local numbers would write the wrong floor. So this reproduces the CI's exact inputs: eslint-results + coverage-report artifacts downloaded from the merged-coverage run on main (29387411665), re-rooted from the runner's paths to the local cwd so extractModuleCoverage can match CRITICAL_MODULE_PATHS, then quality:collect + quality:ratchet --update. Collected output matches the CI's report line for line (branches 78.1, statements/lines 80.8, functions 86.44, chatCore 72.98, combo 85.42, accountFallback 96.78, auth 92.55). Verified: no baseline key added or removed (56 before, 56 after) — only the 12 coverage values moved. The 57-vs-56 metric count between the CI's run and a local one is --allow-missing skipping the metrics only CI collects (mutation scores, CodeQL, bundle size). Worth recording why the improvement appeared now: it is real, but it surfaced because Coverage had been SKIPPED whenever unit shards went red — so the ratchet was passing trivially over ABSENT data. Fixing the shards on #7300 made coverage run and the ratchet finally had something to compare. * fix(stream): reconcile encrypted Codex reasoning visibility without mutating upstream item (#7304) * fix(stream): reconcile encrypted Codex reasoning visibility without mutating upstream item Resolves the collision between two open PRs on ensureVisibleResponsesReasoningSummary: #7095 (xz-dev) found that chat clients see nothing when Codex exposes reasoning only as encrypted_content, and added a visible placeholder — but did so by mutating item.summary in place. #7176 (JxnLexn) found that same mutation corrupts the forwarded response item, discarding the encrypted_content shape Codex needs for follow-up requests, and removed the mutation — but that also silently dropped the placeholder, so chat clients went back to seeing nothing. The mutation existed only so a later line could read the summary text back off the same item. getVisibleResponsesReasoningSummaryText() computes that text without touching the item, so: - synthetic response.reasoning_summary_text.delta / .part.done events still carry the placeholder for chat clients (#7095's goal), and - the forwarded response.output_item.done payload keeps its original encrypted_content intact with no fabricated summary field (#7176's goal). Applied at both call sites #7095 identified: the native Responses passthrough in stream.ts/passthroughTailProcessor.ts, and the Responses-to-Chat-Completions translator in openai-responses.ts. Closes #7095, closes #7176. Co-authored-by: Xiangzhe <xiangzhedev@gmail.com> Co-authored-by: Jan Leon <Jan.gaschler@gmail.com> * test(stream): guard the encrypted-reasoning mutation via the completed backfill path The output_item.done line is echoed verbatim on the wire, so a re-introduced item.summary mutation does NOT surface in that event — verified by re-injecting the mutation, which left the existing assertion green. The mutation does surface in the response.completed snapshot, where the captured reasoning item is re-serialized when upstream sends an empty output (store: false). Adds that case, which fails as expected when the mutation is re-introduced, making the #7176 half of the reconciliation an enforced regression guard rather than an incidental property of the current code path. Co-authored-by: Xiangzhe <xiangzhedev@gmail.com> Co-authored-by: Jan Leon <Jan.gaschler@gmail.com> --------- Co-authored-by: Xiangzhe <xiangzhedev@gmail.com> Co-authored-by: Jan Leon <Jan.gaschler@gmail.com> * chore(ci): stop dependabot proposing typescript majors — peer-blocked by typescript-eslint (#7306) typescript-eslint pins a hard upper bound on its typescript peer (8.64.0 → ">=4.8.4 <6.1.0"). A major TS bump violates it, so the failure is not one check — it is the whole toolchain at once. #7068 is the demonstration: dependabot grouped typescript ^6→^7 with six harmless dev bumps (@types/node, eslint, fast-check, knip, prettier, typescript-eslint) and turned Build, Lint, Quality Ratchet, Unit (6/8, 8/8), Integration (1/2, 2/2) and dast-smoke red in a single PR. The six innocuous updates were blocked by the one that could never pass. Ignoring the major lets the rest of the group flow on its own. TS majors are a toolchain migration and deserve their own PR and their own CI run — not a weekly automated attempt that cannot succeed until typescript-eslint widens the peer. Refs #7068 * test(dashboard): dedicated regression guard for #6815 density guarantee (#7291) * test(dashboard): dedicated regression guard for #6815 density guarantee Coverage for the #6815 multi-column density guarantee was only ever asserted incidentally, by two other guards (#7072, #3520) that pinned the literal sm:grid-cols-2 token. That coupling evaporated the coverage when PR #7027 migrated the component to a container-driven auto-fit template and the literal token was removed from both files. Adds a dedicated guard that simulates, from the shipped className, how many columns the per-group card grid renders at a wide container width -- supporting both the breakpoint-ladder and auto-fit mechanisms this component has shipped with -- and asserts >1 column, without asserting any specific Tailwind token. * chore(changelog): fragment for #7291 density guard * test(ci): mock route bridge surfaces error message, not raw stack (#7354) The E2E mock HTTP server's 500 catch sent error.stack straight to the response body, which CodeQL flags as js/stack-trace-exposure (medium). It's test-only localhost code, but the repo-wide CodeQL ratchet counts open alerts across all branches — so this one alert (baseline 0 → 1) turned the Quality Ratchet red on EVERY open PR into both main and release, masking whatever each PR actually changed. Surface error.message instead: clears the alert, keeps a useful signal for a failing mock route, and doesn't log to stderr (node:test native runner corrupts its report stream on console output). The test only asserts status 200, so the 500 body is not checked. Introduced by the #7304 integration test added this cycle. * ci(release-green): add a main-green arm to detect when main goes red (#7355) The release-green workflow already reproduces the release-equivalent gate on release/** and opens a tracking issue on HARD failures — but main had no such watch. Under the parallel-cycle model main only receives merged work at the release squash, so a gate/infra fix that landed only on the release branch leaves main red the whole cycle, and repo-wide gates (CodeQL alert count, ratchet baselines) turn EVERY PR into main red on a check unrelated to its diff. v3.8.49 hit this 3× in one night. Adds a dedicated main-green job (push to main + the same 3 crons + dispatch) that checks out main literally (no resolver, no injection surface), runs the same validate-release-green.mjs, and opens/updates a '🔴 main branch not green' issue pointing at the companion-PR fix. Gates the existing release-green job with an if: so a push to main doesn't re-validate release and vice-versa; schedule/dispatch sweep both. Detection backstop for the prevention rule in _shared/merge-gates.md §8. * fix(sse): sanitize non-ok Antigravity streaming error body (port from 9router#2461) (#7106) Root cause: the STREAMING branch of AntigravityExecutor.executeOnce() had no !response.ok check at all — it unconditionally wrapped the upstream response body in a pass-through TransformStream, unlike the sibling non-streaming branch which already built a sanitized error via buildAntigravityUpstreamError. When Google's 403 error body was binary/non-UTF8 (observed: gzip-magic-byte payloads), those raw bytes were forwarded verbatim, corrupting the client-visible error message ('[ERROR] [403]: <control-byte garbage>'). Fix: add the same !response.ok guard to the streaming branch, routing through buildAntigravityUpstreamError()/buildErrorBody() (hard rule #12) instead of piping unknown bytes through as if they were an SSE stream. Reported-by: Duongkhanhtool (https://github.com/decolua/9router/issues/2461) * fix(6954,6953): preserve system role + strip empty-signature thinking blocks (#6982) * fix(6954,6953): preserve system role + strip empty-signature thinking blocks #6954 — System turns misattributed as assistant (claude-to-openai.ts:352) The ternary `msg.role === 'user' || msg.role === 'tool' ? 'user' : 'assistant'` mapped any non-user/non-tool role (including 'system') to 'assistant'. Mid-conversation system turns (Claude format) lost their role on translation to OpenAI format, causing them to be treated as assistant output. Fix: add explicit 'system' branch to the ternary. #6953 — Empty-signature thinking blocks poison Anthropic leg (openai-to-claude.ts) Non-Anthropic providers (codex/gpt-5.x) synthesize thinking blocks with signature:''\. On replay, the old code fabricated a DEFAULT_THINKING_CLAUDE_SIGNATURE to fill the empty signature — but Anthropic rejects foreign signatures with HTTP 400, permanently degrading combo/blend routes to codex-only. Fix: strip thinking blocks with empty/missing signatures and redacted_thinking blocks with empty/missing data entirely. They carry no replayable value. Tests: 8 new tests (4 per bug), all passing. Existing #5312 and #5945 regression tests still pass — no interference. * fix(6953): strip only signature:"" thinking blocks, preserve undefined signature CI caught a regression: translator-helper-branches test had a Claude-format thinking block without signature field (undefined) that was being stripped by the original fix. The fix was too aggressive — it stripped both signature:"" (non-Anthropic synthesized) and signature: undefined (legitimate Claude-format). Correct behavior: - signature === "" (empty string): strip — hallmark of codex/gpt-5.x block - signature === undefined: preserve with DEFAULT_THINKING_CLAUDE_SIGNATURE fallback - redacted_thinking data === "": strip - redacted_thinking data === undefined: preserve with fallback Added regression test for undefined-signature preservation. --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * fix(6980): classify Cloudflare AI neuron exhaustion as quota_exhausted (#6983) Cloudflare Workers AI free tier (10k Neurons/day, account-wide) returns 429 with body 'you have used up your daily free allocation of 10,000 neurons' which matched no QUOTA_PATTERNS keyword — falling through to rate_limit (~60s cooldown) instead of quota_exhausted. Two layers: 1. Provider-specific rule for 'cloudflare-ai' in providerRuleRegistry (scope: connection — budget is account-wide, not per-model) 2. Defense-in-depth: /daily free allocation/i in classify429 QUOTA_PATTERNS Tests: 11/11 pass (provider rule + classify429 paths covered). Closes #6980 Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * fix(models): preserve chat-capable image model rows (#7004) Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * fix(sse): register ollama-cloud in USAGE_FETCHER_PROVIDERS (#7026) (#7041) Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * fix(quality): read cognitiveComplexity= machine line in validate-release-green (#7009) (#7042) Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * fix(relay): bound Bifrost stream lifetime (#7093) Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * fix(sse): recognize xiaomi-tokenplan mimo as a thinking-mode model (#7098) * fix(sse): recognize xiaomi-tokenplan mimo as a thinking-mode model (port from 9router#1321) The reasoning_content injector already handles DeepSeek/Kimi/K2/MiniMax thinking-mode upstreams, echoing a placeholder reasoning_content on assistant turns that lack one. Its THINKING_MODEL_PATTERNS list omitted the xiaomi-tokenplan mimo family, so requests through xiaomi-tokenplan/mimo-v2.5-pro still hit upstream's 400 'reasoning_content in the thinking mode must be passed back to the API', making the model unusable in multi-turn conversations (e.g. Codex CLI). Add a /\bmimo\b/i pattern so mimo models get the same treatment. Reported-by: z.wl (@xxue-z) (https://github.com/decolua/9router/issues/1321) * docs(changelog): add fragment for #7098 mimo thinking-model fix * fix(codex): strip regex lookaround from tool schema patterns (#7100) * fix(codex): strip regex lookaround from tool schema patterns (port from 9router#1556) Codex/OpenAI's Responses API rejects JSON Schema pattern fields using regex lookaround (e.g. ^(?=.*@).+$) with a 400 'regex lookaround is not supported' error. The existing numeric-field sanitizer (coerceSchemaNumericFields) was only wired into the translated-request path (openai-to-claude.ts), not the native codex/openai passthrough path (normalizeCodexTools in open-sse/executors/codex/tools.ts), so lookahead/lookbehind patterns reached upstream unmodified and broke tool calls for clients that emit them (e.g. IDE agent harnesses validating an email field). Reported-by: evin (@evinjohnn) (https://github.com/decolua/9router/issues/1556) * chore(changelog): move #1556 entry to changelog.d fragment Consistency with the repo's canonical changelog.d/fixes/ workflow (avoids merge-storm re-conflicts from editing CHANGELOG.md directly). * refactor(codex): table-drive the regex-strip recursion to keep the complexity ratchet at baseline The #1556 lookaround strip walked every sub-schema field with its own copy-pasted if-block (properties / patternProperties / definitions / $defs, then prefixItems / anyOf / oneOf / allOf), pushing stripUnsupportedRegexPatterns past the cyclomatic threshold and check:complexity to 2057 > baseline 2056. Collapse the eight near-identical blocks into two loops over the field-name constants, with the object-map recursion factored into a helper. Same fields, same traversal order, same behavior — complexity is back at baseline 2056 and the #1556 regression tests still pass. * fix(compression): Headroom SmartCrusher skips developer-role messages (port from 9router#2132) (#7102) Root cause: SmartCrusher's system-message guard only excluded role === "system", but Codex CLI (open-sse/executors/codex.ts) sends its instructions/tool-schema turn with role "developer" (the Responses-API equivalent of system used by newer models). Every other system-exclusion guard in this codebase also covers developer (roleNormalizer.ts, contextManager.ts, claudeUpstreamMessages.ts, etc.) except this one, so Headroom happily tabular-compacted JSON arrays embedded in the developer turn (e.g. an update_plan tool schema example), corrupting the instructions the model needs to call the plan tool and breaking Codex CLI plan mode. Fix: extend the guard in crushMessages()/collectCompactableArrays() (smartcrusher.ts) to skip role === "developer" alongside role === "system". Reported-by: SingCJ (https://github.com/decolua/9router/issues/2132) * fix(providers): surface a warning on 404 model_not_found in OpenAI-compatible Check (port from 9router#2032) (#7103) Root cause: validateOpenAICompatibleProvider's chat-completions probe fallback treated ANY 4xx other than 401/403/429/400 as a silent 'credentials valid' pass with no warning, so a bogus/non-standard model id (e.g. Featherless/OpenRouter vendor/model typos) went undetected at Check time. The first real request then hit the upstream 404 model_not_found and the per-model lockout, holding the model unavailable for the configured reset window with no prior indication anything was wrong. User-visible effect: 'Check' now returns valid:true with an explicit warning (including the upstream error message when parseable) whenever the chat probe answers 404, so a bad model id is caught before it reaches production traffic and the lockout. Reported-by: advane204f (https://github.com/decolua/9router/issues/2032) * fix(executors): forward X-Session-ID/X-Title agent metadata headers (#7104) * fix(executors): forward X-Session-ID/X-Title agent metadata headers (port from 9router#2413) Custom agent clients (e.g. non-OpenCode providers) commonly send X-Session-ID and X-Title headers for upstream request tracking/attribution, but forwardOpencodeClientHeaders() only forwarded x-opencode-* keys plus User-Agent, silently dropping these for every client. Extends the existing case-insensitive allowlist forwarding path with x-session-id/x-title. Reported-by: Atikur Rahman Chitholian (@chitholian) (https://github.com/decolua/9router/issues/2413) * chore(changelog): move #2413 entry to changelog.d fragment Consistency with the repo's canonical changelog.d/fixes/ workflow (avoids merge-storm re-conflicts from editing CHANGELOG.md directly). * fix(cli): verify better-sqlite3 native binary is actually loadable (#7105) * fix(cli): verify better-sqlite3 native binary is actually loadable (port from 9router#2493) isBetterSqliteBinaryValid() only checked the .node file's magic bytes (ELF/Mach-O/PE header), never whether the binary was built for the ABI (NODE_MODULE_VERSION) of the Node runtime that loads it. A stale or foreign-ABI binary passed the check and then segfaulted the process on the first database call instead of triggering a rebuild via npmInstallRuntime(). The fix adds a real load probe (require() in a throwaway subprocess) after the magic-byte check, so an incompatible binary is now correctly reported as invalid and the runtime self-heal reinstalls it. Reported-by: Manikandan (@mrprohack) (https://github.com/decolua/9router/issues/2493) * chore(changelog): move #2493 entry to changelog.d fragment Consistency with the repo's canonical changelog.d/fixes/ workflow (avoids merge-storm re-conflicts from editing CHANGELOG.md directly). * fix(sse): handle space-separated arg name/value in Composer tool calls (port from 9router#1811) (#7116) parseInnerCall only split arg segments on a newline between the arg name and its value. Cursor's live Composer/Auto output has been observed using a single space instead, so those segments were treated as one long (space-containing) arg name with an empty value, silently no-opping Write/tool calls for Composer/Auto models. Fall back to splitting on the first whitespace boundary when no newline is present in the segment. Reported-by: way-art (https://github.com/decolua/9router/issues/1811) * fix(cli): remove MITM DNS spoof entries before killing server process (#7117) * fix(cli): remove MITM DNS spoof entries before killing server process (port from 9router#1809) stopMitm() killed the spawned MITM server process first and only removed the /etc/hosts DNS-spoof entries afterward. During that window any client whose DNS still resolved a target host to 127.0.0.1 but whose MITM listener was already dead got connect ECONNREFUSED 127.0.0.1:443 — exactly the community-confirmed workaround (stop DNS before stopping the server) proves. Swap the two steps so DNS is always cleared first, mirroring the ordering already used by repairMitm() and handleExitCleanup(). Reported-by: dionisius95 (https://github.com/decolua/9router/issues/1809) * refactor(mitm): extract repair planning out of manager to respect the file-size cap The #1809 DNS-before-kill ordering fix pushed src/mitm/manager.ts to 813 lines, over the 800-line cap check:file-size enforces for non-frozen files. Move the pure repair-planning pieces (collectManagedHosts, the RepairPlan shape and its filesystem/cert/DNS sweep) into a sibling src/mitm/repair.ts. The in-memory session bookkeeping repairMitm() owns — cached sudo password, orphaned flag, PID file — deliberately stays in manager.ts, so the seam is "plan the repair" vs "own the session". manager.ts is now 731 lines; behavior is unchanged. The DNS-first ordering fix and its regression guard (tests/unit/mitm-stop-dns-before-kill-1809.ts) are untouched and still pass. * fix(mitm): split stopMitm() DNS/kill steps to fix complexity ratchet regression stopMitm()'s new DNS-before-kill ordering (#1809) pushed its cyclomatic complexity to 18 (max 15), regressing the complexity ratchet from 2056 to 2057. Extract the DNS-removal step and the process-kill step (in-memory + PID-file fallback) into two private helpers, mirroring the existing performRepairSteps() extraction pattern in repair.ts. Behavior unchanged; complexity back at 2056 (cognitive-complexity drops to 889, one under baseline). * fix(api): check Vercel SSO-protection PATCH response on relay deploy (#7119) * fix(api): check Vercel SSO-protection PATCH response on relay deploy (port from 9router#1037) The Vercel relay deploy route disabled Deployment Protection (SSO) by firing a PATCH request with .catch(() => {}) and never checking res.ok. When Vercel rejects or no-ops the PATCH (plan doesn't allow disabling protection, an under-scoped token, etc.), the relay was still saved and activated as a healthy proxy pool, and later requests routed through it failed with an undiagnosed 403 Access denied from Vercel's own deployment protection — indistinguishable from an upstream-provider rejection (e.g. Codex/ChatGPT edge-IP blocking). Extract disableSsoProtection() to check the PATCH response and surface an ssoProtectionWarning in the deploy response when it fails, so the failure source can be diagnosed instead of silently masked. Reported-by: Rico Aditya (@ricatix) (https://github.com/decolua/9router/issues/1037) * refactor(api): extract vercel-deploy POST helpers to keep the cognitive-complexity ratchet at baseline The SSO-protection check added to POST pushed its cognitive complexity from 15 to 21, regressing the cognitive-complexity ratchet (891 > baseline 890). Extract two pure helpers with identical behavior: - buildDeployErrorResponse(): the sanitized non-ok Vercel deploy response - resolveSsoProtectionWarning(): the SSO PATCH check + warning string POST now reads as a flat sequence of guards. No behavior change. * fix(combos): reject oversized fusion panels before fan-out (port from 9router#1905) (#7120) A fusion combo fans every panel model out in parallel and buffers each model's full response text in memory simultaneously. With the runtime heap capped by Dockerfile's OMNIROUTE_MEMORY_MB (default 1024MB), a large panel (reported: ~73 models via an 'auto' combo with strategy: fusion) with sizable concurrent responses can exceed the heap ceiling and OOM-crash the whole container instead of failing one request. handleFusionChat now rejects panels above a configurable hard cap (FUSION_DEFAULTS.maxPanel = 40, overridable per-combo via fusionTuning.maxPanel) with a clean 400 before fan-out begins. Reported-by: Phong Vu (@fontvu) (https://github.com/decolua/9router/issues/1905) * fix(combo): detect empty content_block in streaming SSE peek (#7121) * fix(combo): detect empty content_block in streaming SSE peek (port from 9router#1382) The bounded SSE peek in validateResponseQuality() treated ANY content_block_start/delta/stop event as proof of real output and stopped buffering immediately, without checking whether the block actually carried text/tool_use content. Some upstreams (reported: DeepSeek, GLM via claude→openai translation) can open and close a text content_block with empty text and no tool_use on tool-heavy requests — the gateway logged success and forwarded a client-visible empty completion, and combo routing never failed over to the next model. Track real content separately from 'a content_block_* event was seen': a tool_use/redacted_thinking block start is self-evidently real signal, a text/thinking block start is not (real content only confirmed via a subsequent delta carrying non-empty text/thinking, or an input_json_delta streaming tool arguments). A completed lifecycle (message_start + message_delta/stop) that never produced real content now fails validateResponseQuality(), matching the existing content_filter empty-stream detection path (#3685). Reported-by: heishen6 (https://github.com/decolua/9router/issues/1382) * refactor(combo): extract SSE lifecycle applier to keep the complexity ratchets at baseline The #1382 empty-content_block peek added a branchy switch inline in parseAccumulatedSse, pushing check:complexity to 2057 > baseline 2056. Move the switch to a module-level applySseLifecycleEvent() and hold the four lifecycle booleans in a single SseLifecycleFlags object threaded through it, so the closure no longer copies flags in and out per event. The per-event predicates (content_block_start / content_block_delta / message_delta) are split into small guard helpers, which keeps the applier flat — cognitive complexity punishes nesting, and an earlier switch-only extraction traded the cyclomatic ratchet for a cognitive regression at 891 > 890. Logic is unchanged; both ratchets are now green (complexity 2055, cognitive-complexity 890) and the #1382 regression tests still pass. * fix(auto): use p95 fallback in speed factors (#7128) Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * Use OpenAI chunks for early chat keepalives (#7136) * Use OpenAI chunks for early chat keepalives * Update keepalive assertion to match chat completion chunk format --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * refactor: reduce cognitive complexity in provider-plugin-manifest route (ratchet gate) Extract isValidServiceModelEntry() and toProviderPluginModel() out of pickServiceModels() so the filter predicate and per-model normalization are named helpers instead of inline closures. This drops the function's cognitive-complexity score from 16 back under the 15 threshold without changing behavior (covered by tests/unit/api/v1/provider-plugin-manifest-route.test.ts). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * ci: retrigger with fresh base snapshot (PR Test Policy ran against a stale GITHUB_BASE_SHA) Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * [needs-vps] fix(dashboard): add vision-capability toggle for custom OpenAI-compatible models (#7124) * fix(dashboard): add vision-capability toggle for custom OpenAI-compatible models (port from 9router#1904) detectVisionInput()/getCustomVisionCapabilityFields() already honoured an explicit supportsVision flag on a custom-model record, but there was no way to set it: the POST/PUT /api/provider-models Zod schema and updateCustomModel()/addCustomModel() silently dropped the field, and the 'Custom Models' add/edit UI had no checkbox at all. Self-hosted/local backends that don't self-report an image input modality (OpenRouter-style architecture.input_modalities) therefore had no way to be flagged vision-capable, so the vision tag never appeared and image inputs were rejected. Reported-by: nguyenphi37 (https://github.com/decolua/9router/issues/1904) * refactor(dashboard): extract providerCredentialText from providerPageHelpers to respect the file-size gate providerPageHelpers.ts is a frozen god-file (cap 1053, split(\n).length metric) and this PR's own +3 lines (the #1904 supportsVision field) pushed it to 1054, failing check:file-size. Extract the cohesive providerText utility + the 4 web-session-credential label/hint/title helpers into a new leaf module (providerCredentialText.ts), re-exported from providerPageHelpers.ts for backward compatibility so all existing import sites keep working unchanged. File now sits at 946 lines, well under the frozen cap. * refactor(db): extract tri-state override helper to keep the complexity ratchet at baseline The #1904 supportsVision override added a second copy of the "absent keeps / null clears / else coerce" block already used by preserveOpenAIDeveloperRole, pushing updateCustomModel to 84 lines and check:complexity to 2057 > 2056. The file-size failure was masking this one: the gate exits on its first red, so complexity never ran until providerPageHelpers was back under its cap. Fold both blocks into applyTriStateBooleanOverride(). Behavior is unchanged — updateCustomModel is back under max-lines-per-function and the global count returns to the 2056 baseline (cognitive-complexity stays at 890). * [needs-vps] fix(dashboard): align onboarding tier content (#7125) * fix(dashboard): align onboarding welcome feature cards vertically * fix(dashboard): align onboarding tier content * chore: scope onboarding PR to UI fix * i18n(pt-BR): add onboarding.tier.flowCaption + afterSetup keys The two new tier keys added to en.json were missing from pt-BR.json, tripping the i18n-pt-br no-drift test (#6695). Add their pt-BR translations. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * fix(ci): fetch full base history in pr-test-policy (shallow graft broke merge-base) (#7501) With --depth=1 the base ref is grafted, so 'git diff base...HEAD' resolves a wrong merge-base for PR branches that recently merged the release branch. The three-dot diff then attributes ALREADY-MERGED sibling PRs' changes to the PR under test, producing false high-signal reds (deleted test files / weakened asserts that exist in no ref reachable from the PR). Observed live on #7329: the job blamed it for tests/unit/ui/provider-plan-config.test.tsx (deleted by an unrelated merged PR) and for #7106's antigravity files. Local reproduction with full history returns PASS for the same head. The job's checkout is already fetch-depth: 0, so the full base fetch only updates the ref — negligible cost. * [needs-vps] fix(electron): materialize Turbopack hashed-module symlinks during packaging (#6724, #6594) (#6794) * fix(electron): materialize Turbopack hashed-module symlinks during packaging Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR); keeps only the author's changes. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(electron): actually enable materializeSymlinks on the electron standalone path The option existed in assembleStandalone but no production callsite passed it, so packaged builds still shipped absolute symlinks into the build machine's worktree for Turbopack hashed externals (better-sqlite3-<hash>, sqlite-vec-<hash>) — verified by dpkg -c on a freshly built .deb. One-line enablement on the electron prepare path, which is exactly the surface #6724/#6594 report. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: huohua-dev <258873123+huohua-dev@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(grok): strip reasoningEffort for grok cli models (#6938) Co-authored-by: minisforum <no@mail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> * chore(quality): rebaseline zizmor 169->175 (cycle workflow drift) +6 from v3.8.48/v3.8.49 workflow changes (npm-publish WS1.3 #7092, electron-release, nightly-compat, nightly-release-green, CI restructures incl. #7501). Breakdown vs v3.8.47: +3 unpinned-uses (@vN convention), +2 cache-poisoning (own release-workflow artifact upload/cache -- operator-controlled, not fork-PR exploitable), +1 excessive-permissions (nightly-compat issues perm). No new template-injection/artipacked/dangerous-triggers. Measured zizmor 1.25.2 = 175 onda3a0be69. Unblocks Quality Gates (Extended) for #7329. * ci: re-trigger against release with #7501 (pr-test-policy full fetch) + zizmor 175 baseline Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Chirag Singhal <76880977+chirag127@users.noreply.github.com> Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com> Co-authored-by: Ronaldo Davi <ronaldodavi@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Andrew Munsell <andrew@wizardapps.net> Co-authored-by: WITALO ROCHA <witalo_rocha@hotmail.com> Co-authored-by: Wital <witalorocha216@gmail.com> Co-authored-by: Aoxiong Yin <i@yinaoxiong.cn> Co-authored-by: Andrew B. <37745667+AndrianBalanescu@users.noreply.github.com> Co-authored-by: Andrian B. <andrewbalanesq@gmail.com> Co-authored-by: Xiangzhe <32761048+xz-dev@users.noreply.github.com> Co-authored-by: Jon Bailey <297513015+Pitchfork-and-Torch@users.noreply.github.com> Co-authored-by: Pitchfork-and-Torch <Pitchfork-and-Torch@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Samir Abis <me@samirabis.com> Co-authored-by: lucasjustinudin <34107354+lucasjustinudin@users.noreply.github.com> Co-authored-by: chy1211 <31048289+chy1211@users.noreply.github.com> Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com> Co-authored-by: whale9820 <whale9820@users.noreply.github.com> Co-authored-by: anhdiepmmk <n08ni.dieppn@gmail.com> Co-authored-by: Septianata Rizky Pratama <19322988+ianriizky@users.noreply.github.com> Co-authored-by: NOXX - Commiter <artur1992123@mail.ru> Co-authored-by: backryun <bakryun0718@proton.me> Co-authored-by: lunkerchen <labanchen@gmail.com> Co-authored-by: lunkerchen <lunkerchen@users.noreply.github.com> Co-authored-by: Ray Doan <raydoan.contact@gmail.com> Co-authored-by: kfiramar <83420275+kfiramar@users.noreply.github.com> Co-authored-by: Jan Leon <Jan.gaschler@gmail.com> Co-authored-by: Someres <168349709+quanturbo@users.noreply.github.com> Co-authored-by: MikeTuev <ra9ftm@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Imam Wahyu Widodo <120608486+hajilok@users.noreply.github.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: AgentKiller45 <jamalzzj45@gmail.com> Co-authored-by: judy459 <JUDYZHU459@outlook.com> Co-authored-by: growab <nekron@icloud.com> Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com> Co-authored-by: Markus Hartung <mail@hartmark.se> Co-authored-by: KooshaPari <koosha@phenotype.io> Co-authored-by: Jade Guo <jade.gly@gmail.com> Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com> Co-authored-by: oyi77 <oyi77@users.noreply.github.com> Co-authored-by: Dayna Blackwell <dayna@blackwell-systems.com> Co-authored-by: backryun <backryun@daonlab.local> Co-authored-by: brick30llc-ctrl <brick30llc@gmail.com> Co-authored-by: brick30llc-ctrl <admin@brick30.com> Co-authored-by: Saren <saren@dumstruck.com> Co-authored-by: Rafael Dias Zendron <mmmarckos@gmail.com> Co-authored-by: Xiangzhe <xiangzhedev@gmail.com> Co-authored-by: Rafael Dias Zendron <rafael.zendron22@gmail.com> Co-authored-by: KooshaPari <62650152+KooshaPari@users.noreply.github.com> Co-authored-by: Wibias <37517432+Wibias@users.noreply.github.com> Co-authored-by: huohua-dev <celentanohertor@gmail.com> Co-authored-by: huohua-dev <258873123+huohua-dev@users.noreply.github.com> Co-authored-by: CitrusIce <31264099+CitrusIce@users.noreply.github.com> Co-authored-by: minisforum <no@mail.com>
🚀 OmniRoute — The Free AI Gateway (हिन्दी)
🌐 Languages: 🇺🇸 English · 🇸🇦 ar · 🇧🇬 bg · 🇧🇩 bn · 🇨🇿 cs · 🇩🇰 da · 🇩🇪 de · 🇪🇸 es · 🇮🇷 fa · 🇫🇮 fi · 🇫🇷 fr · 🇮🇳 gu · 🇮🇱 he · 🇮🇳 hi · 🇭🇺 hu · 🇮🇩 id · 🇮🇹 it · 🇯🇵 ja · 🇰🇷 ko · 🇮🇳 mr · 🇲🇾 ms · 🇳🇱 nl · 🇳🇴 no · 🇵🇭 phi · 🇵🇱 pl · 🇵🇹 pt · 🇧🇷 pt-BR · 🇷🇴 ro · 🇷🇺 ru · 🇸🇰 sk · 🇸🇪 sv · 🇰🇪 sw · 🇮🇳 ta · 🇮🇳 te · 🇹🇭 th · 🇹🇷 tr · 🇺🇦 uk-UA · 🇵🇰 ur · 🇻🇳 vi · 🇨🇳 zh-CN
Never stop coding. Smart routing to FREE & low-cost AI models with automatic fallback.
Your universal API proxy — one endpoint, 100+ providers, zero downtime. Now with MCP Server (25 tools), A2A Protocol, Memory/Skills Systems & Electron Desktop App.
Chat Completions • Embeddings • Image Generation • Video • Music • Audio • Reranking • Web Search • MCP Server • A2A Protocol • 100% TypeScript
🌐 Available in: 🇺🇸 English | 🇧🇷 Português (Brasil) | 🇪🇸 Español | 🇫🇷 Français | 🇮🇹 Italiano | 🇷🇺 Русский | 🇨🇳 中文 (简体) | 🇩🇪 Deutsch | 🇮🇳 हिन्दी | 🇹🇭 ไทย | 🇺🇦 Українська | 🇸🇦 العربية | 🇯🇵 日本語 | 🇻🇳 Tiếng Việt | 🇧🇬 Български | 🇩🇰 Dansk | 🇫🇮 Suomi | 🇮🇱 עברית | 🇭🇺 Magyar | 🇮🇩 Bahasa Indonesia | 🇰🇷 한국어 | 🇲🇾 Bahasa Melayu | 🇳🇱 Nederlands | 🇳🇴 Norsk | 🇵🇹 Português (Portugal) | 🇷🇴 Română | 🇵🇱 Polski | 🇸🇰 Slovenčina | 🇸🇪 Svenska | 🇵🇭 Filipino | 🇨🇿 Čeština
🖼️ Main Dashboard
📸 Dashboard Preview
Click to see dashboard screenshots
| Page | Screenshot |
|---|---|
| Providers | ![]() |
| Combos | ![]() |
| Analytics | ![]() |
| Health | ![]() |
| Translator | ![]() |
| Settings | ![]() |
| CLI Tools | ![]() |
| Usage Logs | ![]() |
| Endpoints | ![]() |
🤖 Free AI Provider for your favorite coding agents
Connect any AI-powered IDE or CLI tool through OmniRoute — free API gateway for unlimited coding.
|
OpenClaw ⭐ 205K |
NanoBot ⭐ 20.9K |
PicoClaw ⭐ 14.6K |
ZeroClaw ⭐ 9.9K |
IronClaw ⭐ 2.1K |
|
OpenCode ⭐ 106K |
Codex CLI ⭐ 60.8K |
Claude Code ⭐ 67.3K |
Kilo Code ⭐ 15.5K |
📡 All agents connect via http://localhost:20128/v1 or http://cloud.omniroute.online/v1 — one config, unlimited models and quota
🤔 Why OmniRoute?
Stop wasting money and hitting limits:
Subscription quota expires unused every month
Rate limits stop you mid-coding
Expensive APIs ($20-50/month per provider)
Manual switching between providers
OmniRoute solves this:
- ✅ Maximize subscriptions - Track quota, use every bit before reset
- ✅ Auto fallback - Subscription → API Key → Cheap → Free, zero downtime
- ✅ Multi-account - Round-robin between accounts per provider
📧 Support
💬 Join our community! WhatsApp Group — Get help, share tips, and stay updated.
- Website: omniroute.online
- GitHub: github.com/diegosouzapw/OmniRoute
- Issues: github.com/diegosouzapw/OmniRoute/issues
- WhatsApp: Community Group
- Contributing: See CONTRIBUTING.md, open a PR, or pick a
good first issue
🐛 Reporting a Bug?
When opening an issue, please run the system-info command and attach the generated file:
npm run system-info
This generates a system-info.txt with your Node.js version, OmniRoute version, OS details, installed CLI tools (qoder, gemini, claude, codex, antigravity, droid, etc.), Docker/PM2 status, and system packages — everything we need to reproduce your issue quickly. Attach the file directly to your GitHub issue.
🔄 How It Works
┌─────────────┐
│ Your CLI │ (Claude Code, Codex, OpenClaw, Cursor, Cline...)
│ Tool │
└──────┬──────┘
│ http://localhost:20128/v1
↓
┌─────────────────────────────────────────┐
│ OmniRoute (Smart Router) │
│ • Format translation (OpenAI ↔ Claude) │
│ • Quota tracking + Embeddings + Images │
│ • Auto token refresh │
└──────┬──────────────────────────────────┘
│
├─→ [Tier 1: SUBSCRIPTION] Claude Code, Codex
│ ↓ quota exhausted
├─→ [Tier 2: API KEY] DeepSeek, Groq, xAI, Mistral, NVIDIA NIM, etc.
│ ↓ budget limit
├─→ [Tier 3: CHEAP] GLM ($0.6/1M), MiniMax ($0.2/1M)
│ ↓ budget limit
└─→ [Tier 4: FREE] Qoder, Qwen, Kiro (unlimited)
Result: Never stop coding, minimal cost
🎯 What OmniRoute Solves — 30 Real Pain Points & Use Cases
Every developer using AI tools faces these problems daily. OmniRoute was built to solve them all — from cost overruns to regional blocks, from broken OAuth flows to protocol operations and enterprise observability.
💸 1. "I pay for an expensive subscription but still get interrupted by limits"
Developers pay $20–200/month for Claude Pro, Codex Pro, or GitHub Copilot. Even paying, quota has a ceiling — 5h of usage, weekly limits, or per-minute rate limits. Mid-coding session, the provider stops responding and the developer loses flow and productivity.
How OmniRoute solves it:
- Smart 4-Tier Fallback — If subscription quota runs out, automatically redirects to API Key → Cheap → Free with zero manual intervention
- Provider Limits Tracking — Cached quota snapshots refresh on a server-side schedule (default
PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70) with manual refresh available in the UI - Multi-Account Support — Multiple accounts per provider with auto round-robin — when one runs out, switches to the next
- Custom Combos — Customizable fallback chains with 13 balancing strategies (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, strict-random, auto, lkgp, context-optimized, context-relay)
- Structured Combo Builder — Build combos step-by-step with explicit provider + model + account selection, including repeated providers and fixed-account targets
- Quota-Aware P2C — Power-of-two account selection now factors quota headroom, backoff, recent errors, and consecutive use
- Codex Business Quotas — Business/Team workspace quota monitoring directly in the dashboard
🔌 2. "I need to use multiple providers but each has a different API"
OpenAI uses one format, Claude (Anthropic) uses another, Gemini yet another. If a dev wants to test models from different providers or fallback between them, they need to reconfigure SDKs, change endpoints, deal with incompatible formats. Custom providers (FriendLI, NIM) have non-standard model endpoints.
How OmniRoute solves it:
- Unified Endpoint — A single
http://localhost:20128/v1serves as proxy for all 100+ providers - Format Translation — Automatic and transparent: OpenAI ↔ Claude ↔ Gemini ↔ Responses API
- Response Sanitization — Strips non-standard fields (
x_groq,usage_breakdown,service_tier) that break OpenAI SDK v1.83+ - Role Normalization — Converts
developer→systemfor non-OpenAI providers;system→userfor GLM/ERNIE - Think Tag Extraction — Extracts
<think>blocks from models like DeepSeek R1 into standardizedreasoning_content - Structured Output for Gemini —
json_schema→responseMimeType/responseSchemaautomatic conversion streamdefaults tofalse— Aligns with OpenAI spec, avoiding unexpected SSE in Python/Rust/Go SDKs
🌐 3. "My AI provider blocks my region/country"
Providers like OpenAI/Codex block access from certain geographic regions. Users get errors like unsupported_country_region_territory during OAuth and API connections. This is especially frustrating for developers from developing countries.
How OmniRoute solves it:
- 3-Level Proxy Config — Configurable proxy at 3 levels: global (all traffic), per-provider (one provider only), and per-connection/key
- Color-Coded Proxy Badges — Visual indicators: 🟢 global proxy, 🟡 provider proxy, 🔵 connection proxy, always showing the IP
- OAuth Token Exchange Through Proxy — OAuth flow also goes through the proxy, solving
unsupported_country_region_territory - Connection Tests via Proxy — Connection tests use the configured proxy (no more direct bypass)
- SOCKS5 Support — Full SOCKS5 proxy support for outbound routing
- TLS Fingerprint Spoofing — Browser-like TLS fingerprint via
wreq-jsto bypass bot detection - 🔏 CLI Fingerprint Matching — Reorders headers and body fields to match native CLI binary signatures, drastically reducing account flagging risk. The proxy IP is preserved — you get both stealth and IP masking simultaneously
🆓 4. "I want to use AI for coding but I have no money"
Not everyone can pay $20–200/month for AI subscriptions. Students, devs from emerging countries, hobbyists, and freelancers need access to quality models at zero cost.
How OmniRoute solves it:
- Ollama Cloud — Cloud-hosted Ollama models at
api.ollama.comwith free "Light usage" tier; useollamacloud/<model>prefix - Free-Only Combos — Chain
if/kimi-k2-thinking → qw/qwen3-coder-plus= $0/month with zero downtime - NVIDIA NIM Free Access — ~40 RPM dev-forever free access to 70+ models at build.nvidia.com (transitioning from credits to pure rate limits)
- Cost Optimized Strategy — Routing strategy that automatically chooses the cheapest available provider
🔒 5. "I need to protect my AI gateway from unauthorized access"
When exposing an AI gateway to the network (LAN, VPS, Docker), anyone with the address can consume the developer's tokens/quota. Without protection, APIs are vulnerable to misuse, prompt injection, and abuse.
How OmniRoute solves it:
- API Key Management — Generation, rotation, and scoping per provider with a dedicated
/dashboard/api-managerpage - Model-Level Permissions — Restrict API keys to specific models (
openai/*, wildcard patterns), with Allow All/Restrict toggle - API Endpoint Protection — Require a key for
/v1/modelsand block specific providers from the listing - Auth Guard + CSRF Protection — All dashboard routes protected with
withAuthmiddleware + CSRF tokens - Rate Limiter — Per-IP rate limiting with configurable windows
- IP Filtering — Allowlist/blocklist for access control
- Prompt Injection Guard — Sanitization against malicious prompt patterns
- AES-256-GCM Encryption — Credentials encrypted at rest
🛑 6. "My provider went down and I lost my coding flow"
AI providers can become unstable, return 5xx errors, or hit temporary rate limits. If a dev depends on a single provider, they're interrupted. Without circuit breakers, repeated retries can crash the application.
How OmniRoute solves it:
- Request Queue & Pacing — Per-connection request buckets smooth bursts before they hit upstream rate caps
- Connection Cooldown — A single connection cools down after retryable failures with optional upstream
Retry-Afterhints and exponential backoff - Provider Circuit Breaker — The provider only trips after fallback is exhausted and the provider request still fails with provider-wide transient errors; connection-scoped
429rate limits stay in Connection Cooldown - Wait For Cooldown — The server can wait for the earliest connection cooldown to expire and retry the same client request automatically
- Anti-Thundering Herd — Mutex + semaphore protection against concurrent retry storms
- Combo Fallback Chains — If the primary provider fails, automatically falls through the chain with no intervention
- Health Dashboard — Uptime monitoring, provider circuit breaker states, cooldowns, cache stats, p50/p95/p99 latency
🔧 7. "Configuring each AI tool is tedious and repetitive"
How OmniRoute solves it:
- CLI Tools Dashboard — Dedicated page with one-click setup for Claude Code, Codex CLI, OpenClaw, Kilo Code, Antigravity, Cline
- GitHub Copilot Config Generator — Generates
chatLanguageModels.jsonfor VS Code with bulk model selection - Onboarding Wizard — Guided 4-step setup for first-time users
- One endpoint, all models — Configure
http://localhost:20128/v1once, access 100+ providers
🔑 8. "Managing OAuth tokens from multiple providers is hell"
Claude Code, Codex, Copilot — all use OAuth 2.0 with expiring tokens. Developers need to re-authenticate constantly, deal with client_secret is missing, redirect_uri_mismatch, and failures on remote servers. OAuth on LAN/VPS is particularly problematic.
How OmniRoute solves it:
- Auto Token Refresh — OAuth tokens refresh in background before expiration
- OAuth 2.0 (PKCE) Built-in — Automatic flow for Claude Code, Codex, Copilot, Kiro, Qwen, Qoder
- Multi-Account OAuth — Multiple accounts per provider via JWT/ID token extraction
- OAuth LAN/Remote Fix — Private IP detection for
redirect_uri+ manual URL mode for remote servers - OAuth Behind Nginx — Uses
window.location.originfor reverse proxy compatibility - Remote OAuth Guide — Step-by-step guide for Google Cloud credentials on VPS/Docker
📊 9. "I don't know how much I'm spending or where"
Developers use multiple paid providers but have no unified view of spending. Each provider has its own billing dashboard, but there's no consolidated view. Unexpected costs can pile up.
How OmniRoute solves it:
- Cost Analytics Dashboard — Per-token cost tracking and budget management per provider
- Budget Limits per Tier — Spending ceiling per tier that triggers automatic fallback
- Per-Model Pricing Configuration — Configurable prices per model
- Usage Statistics Per API Key — Request count and last-used timestamp per key
- Analytics Dashboard — Stat cards, model usage chart, provider table with success rates and latency
🐛 10. "I can't diagnose errors and problems in AI calls"
When a call fails, the dev doesn't know if it was a rate limit, expired token, wrong format, or provider error. Fragmented logs across different terminals. Without observability, debugging is trial-and-error.
How OmniRoute solves it:
- Unified Logs Dashboard — 4 tabs: Request Logs, Proxy Logs, Audit Logs, Console
- Console Log Viewer — Real-time terminal-style viewer with color-coded levels, auto-scroll, search, filter
- SQLite Summary Logs — Request and proxy log indexes stay queryable across restarts without loading large payload blobs into SQLite
- Translator Playground — 4 debugging modes: Playground (format translation), Chat Tester (round-trip), Test Bench (batch), Live Monitor (real-time)
- Request Telemetry — p50/p95/p99 latency + X-Request-Id tracing
- File-Based Detail Artifacts — App logs rotate by size, retention days, and archive count; detailed request/response payloads live in
DATA_DIR/call_logs/and rotate independently of SQLite summaries - System Info Report —
npm run system-infogeneratessystem-info.txtwith your full environment (Node version, OmniRoute version, OS, CLI tools, Docker/PM2 status). Attach it when reporting issues for instant triage.
🏗️ 11. "Deploying and maintaining the gateway is complex"
Installing, configuring, and maintaining an AI proxy across different environments (local, VPS, Docker, cloud) is labor-intensive. Problems like hardcoded paths, EACCES on directories, port conflicts, and cross-platform builds add friction.
How OmniRoute solves it:
- npm global install —
npm install -g omniroute && omniroute— done - Docker Multi-Platform — AMD64 + ARM64 native (Apple Silicon, AWS Graviton, Raspberry Pi)
- Docker Compose Profiles —
base(no CLI tools) andcli(with Claude Code, Codex, OpenClaw) - Electron Desktop App — Native app for Windows/macOS/Linux with system tray, auto-start, offline mode
- Split-Port Mode — API and Dashboard on separate ports for advanced scenarios (reverse proxy, container networking)
- Cloud Sync — Config synchronization across devices via Cloudflare Workers
- DB Backups — Automatic backup, restore, export and import of all settings, with
DISABLE_SQLITE_AUTO_BACKUPfor externally managed backups
🌍 12. "The interface is English-only and my team doesn't speak English"
Teams in non-English-speaking countries, especially in Latin America, Asia, and Europe, struggle with English-only interfaces. Language barriers reduce adoption and increase configuration errors.
How OmniRoute solves it:
- Dashboard i18n — 30 Languages — All 500+ keys translated including Arabic, Bulgarian, Danish, German, Spanish, Finnish, French, Hebrew, Hindi, Hungarian, Indonesian, Italian, Japanese, Korean, Malay, Dutch, Norwegian, Polish, Portuguese (PT/BR), Romanian, Russian, Slovak, Swedish, Thai, Ukrainian, Vietnamese, Chinese, Filipino, English
- RTL Support — Right-to-left support for Arabic and Hebrew
- Multi-Language READMEs — 30 complete documentation translations
- Language Selector — Globe icon in header for real-time switching
🔄 13. "I need more than chat — I need embeddings, images, audio"
AI isn't just chat completion. Devs need to generate images, transcribe audio, create embeddings for RAG, rerank documents, and moderate content. Each API has a different endpoint and format.
How OmniRoute solves it:
- Embeddings —
/v1/embeddingswith 6 providers and 9+ models - Image Generation —
/v1/images/generationswith 10 providers and 20+ models (OpenAI, xAI, Together, Fireworks, Nebius, Hyperbolic, NanoBanana, Antigravity, SD WebUI, ComfyUI) - Text-to-Video —
/v1/videos/generations— ComfyUI (AnimateDiff, SVD) and SD WebUI - Text-to-Music —
/v1/music/generations— ComfyUI (Stable Audio Open, MusicGen) - Audio Transcription —
/v1/audio/transcriptions— Whisper + Nvidia NIM, HuggingFace, Qwen3 - Text-to-Speech —
/v1/audio/speech— ElevenLabs, Nvidia NIM, HuggingFace, Coqui, Tortoise, Qwen3, Inworld, Cartesia, PlayHT, + existing providers - Moderations —
/v1/moderations— Content safety checks - Reranking —
/v1/rerank— Document relevance reranking - Responses API — Full
/v1/responsessupport for Codex
🧪 14. "I have no way to test and compare quality across models"
Developers want to know which model is best for their use case — code, translation, reasoning — but comparing manually is slow. No integrated eval tools exist.
How OmniRoute solves it:
- LLM Evaluations — Golden set testing with 10 pre-loaded cases covering greetings, math, geography, code generation, JSON compliance, translation, markdown, safety refusal
- 4 Match Strategies —
exact,contains,regex,custom(JS function) - Translator Playground Test Bench — Batch testing with multiple inputs and expected outputs, cross-provider comparison
- Chat Tester — Full round-trip with visual response rendering
- Live Monitor — Real-time stream of all requests flowing through the proxy
📈 15. "I need to scale without losing performance"
As request volume grows, without caching the same questions generate duplicate costs. Without idempotency, duplicate requests waste processing. Per-provider rate limits must be respected.
How OmniRoute solves it:
- Semantic Cache — Two-tier cache (signature + semantic) reduces cost and latency
- Request Idempotency — 5s deduplication window for identical requests
- Rate Limit Detection — Per-provider RPM, min gap, and max concurrent tracking
- Request Queue & Pacing — Configurable queue, pacing, and concurrency defaults in Settings → Resilience
- API Key Validation Cache — 3-tier cache for production performance
- Health Dashboard with Telemetry — p50/p95/p99 latency, cache stats, uptime
🤖 16. "I want to control model behavior globally"
Developers who want all responses in a specific language, with a specific tone, or want to limit reasoning tokens. Configuring this in every tool/request is impractical.
How OmniRoute solves it:
- System Prompt Injection — Global prompt applied to all requests
- Thinking Budget Validation — Reasoning token allocation control per request (passthrough, auto, custom, adaptive)
- 9 Routing Strategies — Global strategies that determine how requests are distributed
- Wildcard Router —
provider/*patterns route dynamically to any provider - Combo Enable/Disable Toggle — Toggle combos directly from the dashboard
- Manual Combo Ordering — Drag combo cards by handle and persist the order in SQLite
- Provider Toggle — Enable/disable all connections for a provider with one click
- Blocked Providers — Exclude specific providers from
/v1/modelslisting
🧰 17. "I need MCP tools as first-class product capabilities"
Many AI gateways expose MCP only as a hidden implementation detail. Teams need a visible, manageable operation layer.
How OmniRoute solves it:
- MCP appears in the dashboard navigation and endpoint protocol tab
- Dedicated MCP management page with process, tools, scopes, and audit
- Built-in quick-start for
omniroute --mcpand client onboarding
🧠 18. "I need A2A orchestration with sync + stream task paths"
Agent workflows need both direct replies and long-running streamed execution with lifecycle control.
How OmniRoute solves it:
- A2A JSON-RPC endpoint (
POST /a2a) withmessage/sendandmessage/stream - SSE streaming with terminal state propagation
- Task lifecycle APIs for
tasks/getandtasks/cancel
🛰️ 19. "I need real MCP process health, not guessed status"
Operational teams need to know if MCP is actually alive, not just whether an API is reachable.
How OmniRoute solves it:
- Runtime heartbeat file with PID, timestamps, transport, tool count, and scope mode
- MCP status API combining heartbeat + recent activity
- UI status cards for process/uptime/heartbeat freshness
📋 20. "I need auditable MCP tool execution"
When tools mutate config or trigger ops actions, teams need forensic traceability.
How OmniRoute solves it:
- SQLite-backed audit logging for MCP tool calls
- Filters by tool, success/failure, API key, and pagination
- Dashboard audit table + stats endpoints for automation
🔐 21. "I need scoped MCP permissions per integration"
Different clients should have least-privilege access to tool categories.
How OmniRoute solves it:
- 10 granular MCP scopes for controlled tool access
- Scope enforcement and visibility in MCP management UI
- Safe default posture for operational tooling
⚙️ 22. "I need operational controls without redeploying"
Teams need quick runtime changes during incidents or cost events.
How OmniRoute solves it:
- Switch combo activation directly from MCP dashboard
- Tune queue, cooldown, breaker, and wait settings from the dedicated Resilience page
- Review live provider breaker state from the Health dashboard
🔄 23. "I need live A2A task lifecycle visibility and cancellation"
Without lifecycle visibility, task incidents become hard to triage.
How OmniRoute solves it:
- Task listing/filtering by state/skill with pagination
- Drill-down on task metadata, events, and artifacts
- Task cancellation endpoint and UI action with confirmation
🌊 24. "I need active stream metrics for A2A load"
Streaming workflows require operational insight into concurrency and live connections.
How OmniRoute solves it:
- Active stream counters integrated into A2A status
- Last task timestamp and per-state counts
- A2A dashboard cards for real-time ops monitoring
🪪 25. "I need standard agent discovery for clients"
External clients and orchestrators need machine-readable metadata for onboarding.
How OmniRoute solves it:
- Agent Card exposed at
/.well-known/agent.json - Capabilities and skills shown in management UI
- A2A status API includes discovery metadata for automation
🧭 26. "I need protocol discoverability in the product UX"
If users cannot discover protocol surfaces, adoption and support quality drop.
How OmniRoute solves it:
- Consolidated Endpoints page with tabs for Proxy, MCP, A2A, and API Endpoints
- Inline service status toggles (Online/Offline) for MCP and A2A
- Links from overview to dedicated management tabs
🧪 27. "I need end-to-end protocol validation with real clients"
Mock tests are not enough to validate protocol compatibility before release.
How OmniRoute solves it:
- E2E suite that boots app and uses real MCP SDK client transport
- A2A client tests for discovery, send, stream, get, and cancel flows
- Cross-check assertions against MCP audit and A2A tasks APIs
📡 28. "I need unified observability across all interfaces"
Splitting observability by protocol creates blind spots and longer MTTR.
How OmniRoute solves it:
- Unified dashboards/logs/analytics in one product
- Health + audit + request telemetry across OpenAI, MCP, and A2A layers
- Operational APIs for status and automation
💼 29. "I need one runtime for proxy + tools + agent orchestration"
Running many separate services increases operational cost and failure modes.
How OmniRoute solves it:
- OpenAI-compatible proxy, MCP server, and A2A server in one stack
- Shared auth, resilience, data store, and observability
- Consistent policy model across all interaction surfaces
🚀 30. "I need to ship agentic workflows without glue-code sprawl"
Teams lose velocity when stitching multiple ad-hoc services and scripts.
How OmniRoute solves it:
- Unified endpoint strategy for clients and agents
- Built-in protocol management UIs and smoke validation paths
- Production-ready foundations (security, logging, resilience, backup)
📚 31. "My long sessions crash with 'context_length_exceeded' limits"
During deep debugging, long histories with tool results quickly exceed provider token windows, causing failed requests and orphaned context.
How OmniRoute solves it:
- Proactive Context Compression — Evaluates token budgets before the request hits upstream and proactively prunes old conversation history with a smart binary-search mechanism.
- Structural Integrity Guards — Automatically tracks explicit
tool_usedefinitions and ensures that if a tool input is truncated, its correspondingtool_resultis also safely removed, preventing API validation errors. - Multi-Layer Dropping — Progressively drops system messages, regular messages, and finally enforces strict length limits without breaking conversational logic.
Example Playbooks (Integrated Use Cases)
Playbook A: Maximize paid subscription + cheap backup
Combo: "maximize-claude"
1. cc/claude-opus-4-7
2. glm/glm-4.7
3. if/kimi-k2-thinking
Monthly cost: $20 + small backup spend
Outcome: higher quality, near-zero interruption
Playbook B: Zero-cost coding stack
Combo: "free-forever"
1. if/kimi-k2-thinking (unlimited free)
2. qw/qwen3-coder-plus (unlimited free)
Monthly cost: $0
Outcome: stable free coding workflow
Playbook C: 24/7 always-on fallback chain
Combo: "always-on"
1. cc/claude-opus-4-7
2. cx/gpt-5.2-codex
3. glm/glm-4.7
4. minimax/MiniMax-M2.1
5. if/kimi-k2-thinking
Outcome: deep fallback depth for deadline-critical workloads
Playbook D: Agent ops with MCP + A2A
1) Start MCP transport (`omniroute --mcp`) for tool-driven operations
2) Run A2A tasks via `message/send` and `message/stream`
3) Observe via /dashboard/endpoint (MCP and A2A tabs)
4) Toggle services via inline status controls
🆓 Start Free — Zero Configuration Cost
Setup AI coding in minutes at $0/month. Connect these free accounts and use the built-in Free Stack combo.
| Step | Action | Providers Unlocked |
|---|---|---|
| 1 | Connect Kiro (AWS Builder ID OAuth) | Claude Sonnet 4.5, Haiku 4.5 — unlimited |
| 2 | Connect Qoder (Google OAuth) | kimi-k2-thinking, qwen3-coder-plus, deepseek-r1... — unlimited |
| 3 | Connect Qwen (Device Code) | qwen3-coder-plus, qwen3-coder-flash... — unlimited |
| 4 | /dashboard/combos → Free Stack ($0) template |
Round-robin all free providers automatically |
Point any IDE/CLI to: http://localhost:20128/v1 · API Key: any-string · Done.
Optional extra coverage (also free): Groq API key (30 RPM free), NVIDIA NIM (40 RPM free, 70+ models), Cerebras (1M tok/day), LongCat API key (50M tokens/day!), Cloudflare Workers AI (10K Neurons/day, 50+ models).
त्वरित प्रारंभ
1) Install and run
npm install -g omniroute
omniroute
pnpm users: Pass
--allow-buildat install time to enable native build scripts required bybetter-sqlite3and@swc/core(theapprove-builds -gcommand is not supported for global installs on pnpm v11):pnpm add -g omniroute@latest --allow-build=better-sqlite3 --allow-build=@swc/core omniroute
Dashboard opens at http://localhost:20128 and API base URL is http://localhost:20128/v1.
Arch Linux (AUR)
Arch Linux users can install the AUR package, which installs OmniRoute and provides a systemd user service:
yay -S omniroute-bin
systemctl --user enable --now omniroute.service
| Command | Description |
|---|---|
omniroute |
Start server (PORT=20128, API and dashboard on same port) |
omniroute --port 3000 |
Set canonical/API port to 3000 |
omniroute --mcp |
Start MCP server (stdio transport) |
omniroute --no-open |
Don't auto-open browser |
omniroute --help |
Show help |
Optional split-port mode:
PORT=20128 DASHBOARD_PORT=20129 omniroute
# API: http://localhost:20128/v1
# Dashboard: http://localhost:20129
2) Uninstalling
When you no longer need OmniRoute, we provide two quick scripts for a clean removal:
| Command | Action |
|---|---|
npm run uninstall |
Removes the system app but keeps your DB and configurations in ~/.omniroute. |
npm run uninstall:full |
Removes the app AND permanently erases all configurations, keys, and databases. |
Note: To run these commands, navigate to the OmniRoute project folder (if you cloned it) and run them. Alternatively, if globally installed, you can simply run
npm uninstall -g omniroute.
Long-Running Streaming Timeouts
For most deployments, you only need:
| Variable | Default | Purpose |
|---|---|---|
REQUEST_TIMEOUT_MS |
600000 |
Shared baseline for upstream response-start timeout, hidden Undici timeouts, TLS fingerprint requests, and API bridge request/proxy timeouts |
STREAM_IDLE_TIMEOUT_MS |
inherits REQUEST_TIMEOUT_MS |
Maximum gap between streaming chunks before OmniRoute aborts the SSE stream |
Backward compatibility is preserved: existing FETCH_TIMEOUT_MS, API_BRIDGE_PROXY_TIMEOUT_MS, and other per-layer timeout vars still work and override the shared baseline.
For Claude Code-compatible upstreams (anthropic-compatible-cc-*), OmniRoute also derives the outbound X-Stainless-Timeout header from the resolved fetch timeout so provider-side read timeouts stay aligned with your env configuration.
For third-party Claude Code-compatible reverse proxies, OmniRoute keeps the default
anthropic-beta set conservative and, when Client Cache Control is left on Auto,
only forwards client-provided cache_control markers. If the request does not include
cache_control, OmniRoute does not inject bridge-owned markers.
Advanced overrides are available if you need finer control:
| Variable | Default | Purpose |
|---|---|---|
FETCH_TIMEOUT_MS |
inherits REQUEST_TIMEOUT_MS |
Upstream response-start timeout used until response headers arrive |
FETCH_HEADERS_TIMEOUT_MS |
inherits FETCH_TIMEOUT_MS |
Undici time limit for receiving upstream response headers |
FETCH_BODY_TIMEOUT_MS |
inherits FETCH_TIMEOUT_MS |
Undici time limit between upstream body chunks (0 disables it) |
FETCH_CONNECT_TIMEOUT_MS |
30000 |
Undici TCP connect timeout |
FETCH_KEEPALIVE_TIMEOUT_MS |
4000 |
Undici idle keep-alive socket timeout |
TLS_CLIENT_TIMEOUT_MS |
inherits FETCH_TIMEOUT_MS |
Timeout for TLS fingerprint requests made through wreq-js |
API_BRIDGE_PROXY_TIMEOUT_MS |
inherits REQUEST_TIMEOUT_MS or 600000 |
Timeout for /v1 proxy forwarding from API port to dashboard port |
API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS |
max(API_BRIDGE_PROXY_TIMEOUT_MS, 300000) |
Incoming request timeout on the API bridge server |
API_BRIDGE_SERVER_HEADERS_TIMEOUT_MS |
60000 |
Incoming header timeout on the API bridge server |
API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS |
5000 |
Keep-alive timeout on the API bridge server |
API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS |
0 |
Socket inactivity timeout on the API bridge server (0 disables it) |
For streaming requests, FETCH_TIMEOUT_MS only covers connection setup / waiting for the first upstream response. Once the stream is active, OmniRoute will only abort on an actual stall (STREAM_IDLE_TIMEOUT_MS) or Undici body inactivity (FETCH_BODY_TIMEOUT_MS).
If you run OmniRoute behind Nginx, Caddy, Cloudflare, or another reverse proxy, make sure the proxy timeouts are also higher than your OmniRoute stream/fetch timeouts.
2) Connect providers and create your API key
- Open Dashboard →
Providersand connect at least one provider (OAuth or API key). - Open Dashboard →
Endpointsand create an API key. - (Optional) Open Dashboard →
Combosand set your fallback chain.
3) Point your coding tool to OmniRoute
Base URL: http://localhost:20128/v1
API Key: [copy from Endpoint page]
Model: if/kimi-k2-thinking (or any provider/model prefix)
4) Enable and validate protocols (v2.0)
MCP (for tool-driven operations):
omniroute --mcp
Then connect your MCP client over stdio and test tools like:
omniroute_get_healthomniroute_list_combos
A2A (for agent-to-agent workflows):
curl http://localhost:20128/.well-known/agent.json
curl -X POST http://localhost:20128/a2a \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":"quickstart","method":"message/send","params":{"skill":"quota-management","messages":[{"role":"user","content":"Give me a short quota summary."}]}}'
5) Validate everything end-to-end (recommended)
npm run test:protocols:e2e
This suite validates real MCP and A2A client flows against a running app.
Alternative: run from source
cp .env.example .env
npm install
PORT=20128 DASHBOARD_PORT=20129 NEXT_PUBLIC_BASE_URL=http://localhost:20129 npm run dev
Void Linux (`xbps-src` template)
For Void Linux users, you can build a native package using xbps-src. Save this block as srcpkgs/omniroute/template:
# Template file for 'omniroute'
pkgname=omniroute
version=3.4.1
revision=1
hostmakedepends="nodejs python3 make"
depends="openssl"
short_desc="Universal AI gateway with smart routing for multiple LLM providers"
maintainer="zenobit <zenobit@disroot.org>"
license="MIT"
homepage="https://github.com/diegosouzapw/OmniRoute"
distfiles="https://github.com/diegosouzapw/OmniRoute/archive/refs/tags/v${version}.tar.gz"
checksum=009400afee90a9f32599d8fe734145cfd84098140b7287990183dde45ae2245b
system_accounts="_omniroute"
omniroute_homedir="/var/lib/omniroute"
export NODE_ENV=production
export npm_config_engine_strict=false
export npm_config_loglevel=error
export npm_config_fund=false
export npm_config_audit=false
do_build() {
# Determine target CPU arch for node-gyp
local _gyp_arch
case "$XBPS_TARGET_MACHINE" in
aarch64*) _gyp_arch=arm64 ;;
armv7*|armv6*) _gyp_arch=arm ;;
i686*) _gyp_arch=ia32 ;;
*) _gyp_arch=x64 ;;
esac
# 1) Install all deps – skip scripts (no network in do_build, native modules
# compiled separately below; better-sqlite3 is serverExternalPackage so
# Next.js does not execute it during next build)
NODE_ENV=development npm ci --ignore-scripts
# 2) Build the Next.js standalone bundle
npm run build
# 3) Copy static assets into standalone
cp -r .next/static .next/standalone/.next/static
[ -d public ] && cp -r public .next/standalone/public || true
# 4) Compile better-sqlite3 native binding for the target architecture.
# Use node-gyp directly so CC/CXX from xbps-src cross-toolchain are used
# without npm altering them.
local _node_gyp=/usr/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js
(cd node_modules/better-sqlite3 && node "$_node_gyp" rebuild --arch="$_gyp_arch")
# 5) Place the compiled binding into the standalone bundle
local _bs3_release=.next/standalone/node_modules/better-sqlite3/build/Release
mkdir -p "$_bs3_release"
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node "$_bs3_release/"
# 6) Remove arch-specific sharp bundles – upstream sets images.unoptimized=true
# so sharp is not used at runtime; x64 .so files would break aarch64 strip
rm -rf .next/standalone/node_modules/@img
# 7) Copy pino runtime deps omitted by Next.js static analysis:
# pino-abstract-transport – required by pino's worker thread
# split2 – dep of pino-abstract-transport
# process-warning – dep of pino itself
for _mod in pino-abstract-transport split2 process-warning; do
cp -r "node_modules/$_mod" .next/standalone/node_modules/
done
}
do_check() {
npm run test:unit
}
do_install() {
vmkdir usr/lib/omniroute/.next
vcopy .next/standalone/. usr/lib/omniroute/.next/standalone
# Prevent removal of empty Next.js app router dirs by the post-install hook
for _d in \
.next/standalone/.next/server/app/dashboard \
.next/standalone/.next/server/app/dashboard/settings \
.next/standalone/.next/server/app/dashboard/providers; do
touch "${DESTDIR}/usr/lib/omniroute/${_d}/.keep"
done
cat > "${WRKDIR}/omniroute" <<'EOF'
#!/bin/sh
export PORT="${PORT:-20128}"
export DATA_DIR="${DATA_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/omniroute}"
export APP_LOG_TO_FILE="${APP_LOG_TO_FILE:-false}"
mkdir -p "${DATA_DIR}"
exec node /usr/lib/omniroute/.next/standalone/server.js "$@"
EOF
vbin "${WRKDIR}/omniroute"
}
post_install() {
vlicense LICENSE
}
🐳 Docker
OmniRoute is available as a public Docker image on Docker Hub.
Quick run:
docker run -d \
--name omniroute \
--restart unless-stopped \
--stop-timeout 40 \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:latest
With environment file:
# Copy and edit .env first
cp .env.example .env
docker run -d \
--name omniroute \
--restart unless-stopped \
--stop-timeout 40 \
--env-file .env \
-p 20128:20128 \
-v omniroute-data:/app/data \
diegosouzapw/omniroute:latest
Using Docker Compose:
# Base profile (no CLI tools)
docker compose --profile base up -d
# CLI profile (Claude Code, Codex, OpenClaw built-in)
docker compose --profile cli up -d
Dashboard support for Docker deployments now includes a one-click Cloudflare Quick Tunnel on Dashboard → Endpoints. The first enable downloads cloudflared only when needed, starts a temporary tunnel to your current /v1 endpoint, and shows the generated https://*.trycloudflare.com/v1 URL directly below your normal public URL.
Notes:
- Quick Tunnel URLs are temporary and change after every restart.
- Quick Tunnels are not auto-restored after an OmniRoute or container restart. Re-enable them from the dashboard when needed.
- Managed install currently supports Linux, macOS, and Windows on
x64/arm64. - Managed Quick Tunnels default to HTTP/2 transport to avoid noisy QUIC UDP buffer warnings in constrained container environments. Set
CLOUDFLARED_PROTOCOL=quicorautoif you want a different transport. - Docker images bundle system CA roots and pass them to managed
cloudflared, which avoids TLS trust failures when the tunnel bootstraps inside the container. - SQLite runs in WAL mode.
docker stopshould be allowed to finish so OmniRoute can checkpoint the latest changes back intostorage.sqlite. - The bundled Compose files already set a 40s stop grace period. If you run the image directly, keep
--stop-timeout 40(or similar) so manual stops do not cut off shutdown cleanup. - Set
CLOUDFLARED_BIN=/absolute/path/to/cloudflaredif you want OmniRoute to use an existing binary instead of downloading one.
Using Docker Compose with Caddy (HTTPS Auto-TLS):
OmniRoute can be securely exposed using Caddy's automatic SSL provisioning. Ensure your domain's DNS A record points to your server's IP.
services:
omniroute:
image: diegosouzapw/omniroute:latest
container_name: omniroute
restart: unless-stopped
volumes:
- omniroute-data:/app/data
environment:
- PORT=20128
- NEXT_PUBLIC_BASE_URL=https://your-domain.com
caddy:
image: caddy:latest
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
command: caddy reverse-proxy --from https://your-domain.com --to http://omniroute:20128
volumes:
omniroute-data:
| Image | Tag | Size | Description |
|---|---|---|---|
diegosouzapw/omniroute |
latest |
~250MB | Latest stable release |
diegosouzapw/omniroute |
3.6.2 |
~250MB | Current version |
🖥️ Desktop App — Offline & Always-On
🆕 NEW! OmniRoute is now available as a native desktop application for Windows, macOS, and Linux.
Run OmniRoute as a standalone desktop app — no terminal, no browser, no internet required for local models. The Electron-based app includes:
- 🖥️ Native Window — Dedicated app window with system tray integration
- 🔄 Auto-Start — Launch OmniRoute on system login
- 🔔 Native Notifications — Get alerts for quota exhaustion or provider issues
- ⚡ One-Click Install — NSIS (Windows), DMG (macOS), AppImage (Linux)
- 🌐 Offline Mode — Works fully offline with bundled server
त्वरित प्रारंभ
# Development mode
npm run electron:dev
# Build for your platform
npm run electron:build # Current platform
npm run electron:build:win # Windows (.exe)
npm run electron:build:mac # macOS (.dmg) — x64 & arm64
npm run electron:build:linux # Linux (.AppImage)
System Tray
When minimized, OmniRoute lives in your system tray with quick actions:
- Open dashboard
- Change server port
- Quit application
📖 Full documentation: electron/README.md
💰 Pricing at a Glance
| Tier | Provider | Cost | Quota Reset | Best For |
|---|---|---|---|---|
| 💳 SUBSCRIPTION | Claude Code (Pro) | $20/mo | 5h + weekly | Already subscribed |
| Codex (Plus/Pro) | $20-200/mo | 5h + weekly | OpenAI users | |
| GitHub Copilot | $10-19/mo | Monthly | GitHub users | |
| 🔑 API KEY | NVIDIA NIM | FREE (dev forever) | ~40 RPM | 70+ open models |
| Cerebras | FREE (1M tok/day) | 60K TPM / 30 RPM | World's fastest | |
| Groq | FREE (30 RPM) | 14.4K RPD | Ultra-fast Llama/Gemma | |
| DeepSeek V3.2 | $0.27/$1.10 per 1M | None | Best price/quality reasoning | |
| xAI Grok-4 Fast | $0.20/$0.50 per 1M 🆕 | None | Fastest + tool calling, ultralow | |
| xAI Grok-4 (standard) | $0.20/$1.50 per 1M 🆕 | None | Reasoning flagship from xAI | |
| Mistral | Free trial + paid | Rate limited | European AI | |
| OpenRouter | Pay-per-use | None | 100+ models aggr. | |
| 💰 CHEAP | GLM-5 (via Z.AI) 🆕 | $0.5/1M | Daily 10AM | 128K output, newest flagship |
| GLM-4.7 | $0.6/1M | Daily 10AM | Budget backup | |
| MiniMax M2.5 🆕 | $0.3/1M input | 5-hour rolling | Reasoning + agentic tasks | |
| MiniMax M2.1 | $0.2/1M | 5-hour rolling | Cheapest option | |
| Kimi K2.5 (Moonshot API) 🆕 | Pay-per-use | None | Direct Moonshot API access | |
| Kimi K2 | $9/mo flat | 10M tokens/mo | Predictable cost | |
| 🆓 FREE | Qoder | $0 | Unlimited | 5 models unlimited |
| Qwen | $0 | Unlimited | 4 models unlimited | |
| Kiro | $0 | Unlimited | Claude Sonnet/Haiku (AWS Builder) | |
| LongCat Flash-Lite 🆕 | $0 (50M tok/day 🔥) | 1 RPS | Largest free quota on Earth | |
| Pollinations AI 🆕 | $0 (no key needed) | 1 req/15s | GPT-5, Claude, DeepSeek, Llama 4 | |
| Cloudflare Workers AI 🆕 | $0 (10K Neurons/day) | ~150 resp/day | 50+ models, global edge | |
| Scaleway AI 🆕 | $0 (1M tokens total) | Rate limited | EU/GDPR, Qwen3 235B, Llama 70B |
🆕 New models added (Mar 2026): Grok-4 Fast family at $0.20/$0.50/M (benchmarked at 1143ms — 30% faster than Gemini 2.5 Flash), GLM-5 via Z.AI with 128K output, MiniMax M2.5 reasoning, DeepSeek V3.2 updated pricing, Kimi K2.5 via Moonshot direct API.
💡 $0 Combo Stack — The Complete Free Setup:
# 🆓 Ultimate Free Stack 2026 — 11 Providers, $0 Forever
Kiro (kr/) → Claude Sonnet/Haiku UNLIMITED
Qoder (if/) → kimi-k2-thinking, qwen3-coder-plus, deepseek-r1 UNLIMITED
LongCat Lite (lc/) → LongCat-Flash-Lite — 50M tokens/day 🔥
Pollinations (pol/) → GPT-5, Claude, DeepSeek, Llama 4 — no key needed
Qwen (qw/) → qwen3-coder-plus, qwen3-coder-flash, qwen3-coder-next UNLIMITED
Gemini (gemini/) → Gemini 2.5 Flash — 1,500 req/day free API key
Cloudflare AI (cf/) → Llama 70B, Gemma 3, Mistral — 10K Neurons/day
Scaleway (scw/) → Qwen3 235B, Llama 70B — 1M free tokens (EU)
Groq (groq/) → Llama/Gemma ultra-fast — 14.4K req/day
NVIDIA NIM (nvidia/) → 70+ open models — 40 RPM forever
Cerebras (cerebras/) → Llama/Qwen world-fastest — 1M tok/day
Zero cost. Never stops coding. Configure this as one OmniRoute combo and all fallbacks happen automatically — no manual switching ever.
🆓 Free Models — What You Actually Get
All models below are 100% free with zero credit card required. OmniRoute auto-routes between them when one quota runs out — combine them all for an unbreakable $0 combo.
🔵 CLAUDE MODELS (via Kiro — AWS Builder ID)
| Model | Prefix | Limit | Rate Limit |
|---|---|---|---|
claude-sonnet-4.5 |
kr/ |
Unlimited | No reported daily cap |
claude-haiku-4.5 |
kr/ |
Unlimited | No reported daily cap |
claude-opus-4.6 |
kr/ |
Unlimited | Latest Opus via Kiro |
🟢 QODER MODELS (Free PAT via qodercli)
| Model | Prefix | Limit | Rate Limit |
|---|---|---|---|
kimi-k2-thinking |
if/ |
Unlimited | No reported cap |
qwen3-coder-plus |
if/ |
Unlimited | No reported cap |
deepseek-r1 |
if/ |
Unlimited | No reported cap |
minimax-m2.1 |
if/ |
Unlimited | No reported cap |
kimi-k2 |
if/ |
Unlimited | No reported cap |
Recommended connection method: Personal Access Token +
qodercli. Browser OAuth is experimental and disabled by default unlessQODER_OAUTH_*environment variables are configured.
🟡 QWEN MODELS (Device Code Auth)
| Model | Prefix | Limit | Rate Limit |
|---|---|---|---|
qwen3-coder-plus |
qw/ |
Unlimited | No reported cap |
qwen3-coder-flash |
qw/ |
Unlimited | No reported cap |
qwen3-coder-next |
qw/ |
Unlimited | No reported cap |
vision-model |
qw/ |
Unlimited | Multimodal (images) |
⚫ NVIDIA NIM (Free API Key — build.nvidia.com)
| Tier | Daily Limit | Rate Limit | Notes |
|---|---|---|---|
| Free (Dev) | No token cap | ~40 RPM | 70+ models; transitioning to pure rate limits mid-2025 |
Popular free models: moonshotai/kimi-k2.5 (Kimi K2.5), z-ai/glm4.7 (GLM 4.7), deepseek-ai/deepseek-v3.2 (DeepSeek V3.2), nvidia/llama-3.3-70b-instruct, deepseek/deepseek-r1
⚪ CEREBRAS (Free API Key — inference.cerebras.ai)
| Tier | Daily Limit | Rate Limit | Notes |
|---|---|---|---|
| Free | 1M tokens/day | 60K TPM / 30 RPM | World's fastest LLM inference; resets daily |
Available free: llama-3.3-70b, llama-3.1-8b, deepseek-r1-distill-llama-70b
🔴 GROQ (Free API Key — console.groq.com)
| Tier | Daily Limit | Rate Limit | Notes |
|---|---|---|---|
| Free | 14.4K RPD | 30 RPM per model | No credit card; 429 on limit, not charged |
Available free: llama-3.3-70b-versatile, gemma2-9b-it, mixtral-8x7b, whisper-large-v3
🔴 LONGCAT AI (Free API Key — longcat.chat) 🆕
| Model | Prefix | Daily Free Quota | Notes |
|---|---|---|---|
LongCat-Flash-Lite |
lc/ |
50M tokens 💥 | Largest free quota ever |
LongCat-Flash-Chat |
lc/ |
500K tokens | Multi-turn chat |
LongCat-Flash-Thinking |
lc/ |
500K tokens | Reasoning / CoT |
LongCat-Flash-Thinking-2601 |
lc/ |
500K tokens | Jan 2026 version |
LongCat-Flash-Omni-2603 |
lc/ |
500K tokens | Multimodal |
100% free while in public beta. Sign up at longcat.chat with email or phone. Resets daily 00:00 UTC.
🟢 POLLINATIONS AI (No API Key Required) 🆕
| Model | Prefix | Rate Limit | Provider Behind |
|---|---|---|---|
openai |
pol/ |
1 req/15s | GPT-5 |
claude |
pol/ |
1 req/15s | Anthropic Claude |
gemini |
pol/ |
1 req/15s | Google Gemini |
deepseek |
pol/ |
1 req/15s | DeepSeek V3 |
llama |
pol/ |
1 req/15s | Meta Llama 4 Scout |
mistral |
pol/ |
1 req/15s | Mistral AI |
✨ Zero friction: No signup, no API key. Add the Pollinations provider with an empty key field and it works immediately.
🟠 CLOUDFLARE WORKERS AI (Free API Key — cloudflare.com) 🆕
| Tier | Daily Neurons | Equivalent Usage | Notes |
|---|---|---|---|
| Free | 10,000 | ~150 LLM resp / 500s audio / 15K embeds | Global edge, 50+ models |
Popular free models: @cf/meta/llama-3.3-70b-instruct, @cf/google/gemma-3-12b-it, @cf/openai/whisper-large-v3-turbo (free audio!), @cf/qwen/qwen2.5-coder-15b-instruct
Requires API Token + Account ID from dash.cloudflare.com. Store Account ID in provider settings.
🟣 SCALEWAY AI (1M Free Tokens — scaleway.com) 🆕
| Tier | Free Quota | Location | Notes |
|---|---|---|---|
| Free | 1M tokens | 🇫🇷 Paris, EU | No credit card needed within limits |
Available free: qwen3-235b-a22b-instruct-2507 (Qwen3 235B!), llama-3.1-70b-instruct, mistral-small-3.2-24b-instruct-2506, deepseek-v3-0324
EU/GDPR compliant. Get API key at console.scaleway.com.
💡 The Ultimate Free Stack (11 Providers, $0 Forever):
Kiro (kr/) → Claude Sonnet/Haiku UNLIMITED Qoder (if/) → kimi-k2-thinking, qwen3-coder-plus, deepseek-r1 UNLIMITED LongCat Lite (lc/) → LongCat-Flash-Lite — 50M tokens/day 🔥 Pollinations (pol/) → GPT-5, Claude, DeepSeek, Llama 4 — no key needed Qwen (qw/) → qwen3-coder models UNLIMITED Gemini (gemini/) → Gemini 2.5 Flash — 1,500 req/day free Cloudflare AI (cf/) → 50+ models — 10K Neurons/day Scaleway (scw/) → Qwen3 235B, Llama 70B — 1M free tokens (EU) Groq (groq/) → Llama/Gemma — 14.4K req/day ultra-fast NVIDIA NIM (nvidia/) → 70+ open models — 40 RPM forever Cerebras (cerebras/) → Llama/Qwen world-fastest — 1M tok/day
🎙️ Free Transcription Combo
Transcribe any audio/video for $0 — Deepgram leads with $200 free, AssemblyAI $50 fallback, Groq Whisper as unlimited emergency backup.
| Provider | Free Credits | Best Model | Rate Limit |
|---|---|---|---|
| 🟢 Deepgram | $200 free (signup) | nova-3 — best accuracy, 30+ languages |
No RPM limit on free credits |
| 🔵 AssemblyAI | $50 free (signup) | universal-3-pro — chapters, sentiment, PII |
No RPM limit on free credits |
| 🔴 Groq | Free forever | whisper-large-v3 — OpenAI Whisper |
30 RPM (rate limited) |
Suggested combo in /dashboard/combos:
Name: free-transcription
Strategy: Priority
Nodes:
[1] deepgram/nova-3 → uses $200 free first
[2] assemblyai/universal-3-pro → fallback when Deepgram credits run out
[3] groq/whisper-large-v3 → free forever, emergency fallback
Then in /dashboard/media → Transcription tab: upload any audio or video file → select your combo endpoint → get transcription in supported formats.
💡 Key Features
OmniRoute v3.6 is built as an operational platform, not just a relay proxy.
🆕 New — v3.6.x Highlights (Apr 2026)
| Feature | What It Does |
|---|---|
| 🌐 V1 WebSocket Bridge | OpenAI-compatible WebSocket traffic upgraded and proxied via /v1/ws — full streaming over WS with session auth (API key or session cookie) |
| 🔑 Sync Tokens & Config Bundle | Issue/revoke sync tokens for config sync endpoints. Config bundles versioned with ETag for bandwidth-efficient polling |
| 🧠 GLM Thinking (glmt) Preset | GLM Thinking registered first-class: 65 536 max tokens, 24 576 thinking budget, 900s timeout, usage sync & pricing — Claude-compatible API |
| 🔢 Hybrid Token Counting | Uses provider-side /messages/count_tokens when available; falls back to estimation — accurate usage tracking without guessing |
| 🌱 Model Alias Auto-Seed | 30+ cross-proxy dialect aliases normalised at startup — no more routing mismatches |
| 🛡️ Safe Outbound Fetch | All provider validation and model discovery go through a guarded fetch layer blocking private/local URLs with retry, timeout, and SSRF protection |
| ⏳ Wait For Cooldown | Server-side chat retries when every candidate connection is cooling down; configurable enabled, maxRetries, and maxRetryWaitSec |
| 🔍 Runtime Env Validation | Startup validates all env vars with Zod schemas — clear errors for missing secrets, invalid URLs, or wrong types |
| 📋 Compliance Audit Expansion | Structured audit logs with pagination, request context, auth events, provider CRUD events, and SSRF-blocked validation logging |
| 🔐 TPS Log Metric | Log details modal shows Tokens Per Second (TPS) — quick performance at-a-glance for every request |
| 🗑️ Uninstall / Full Uninstall | npm run uninstall keeps data, npm run uninstall:full removes everything — clean removal for all install methods |
| 🔧 OAuth Env Repair | One-click "Repair env" action for OAuth providers restores missing env vars and fixes broken auth state |
| 🔒 Graceful Electron Shutdown | Electron before-quit shuts down Next.js gracefully, preventing SQLite WAL database locks on desktop close |
| 👁️ Model Visibility Toggle | Per-model visibility toggle (👁 icon) with search filter and active-count badge (N/M active) on provider pages |
| 📧 Email Privacy Masking | OAuth account emails masked (di*****@g****.com), full address visible on hover |
| 🔗 Context Relay Strategy | Combo strategy preserving session continuity via structured handoff summaries when accounts rotate mid-conversation |
| 🛡️ Proxy Hardening | Token health check, API key validation, and undici dispatcher all honor proxy config |
| ⚠️ Node.js 24 Login Warning | Login page proactively detects incompatible Node.js versions and shows a clear warning banner |
| 📎 Gemini PDF Attachments | PDF attachments correctly routed to Gemini via inline_data and generic base64 detection |
| 🔒 CodeQL Security Hardening | Resolved SSRF, insecure randomness, polynomial ReDoS, and incomplete URL sanitization alerts |
🆕 New — ClawRouter-Inspired Improvements (Mar 2026)
| Feature | What It Does |
|---|---|
| ⚡ Grok-4 Fast Family | xAI models at $0.20/$0.50/M — benchmarked 1143ms (30% faster than Gemini 2.5 Flash) |
| 🧠 GLM-5 via Z.AI | 128K output context, $0.5/1M — newest flagship from the GLM family |
| 🔮 MiniMax M2.5 | Reasoning + agentic tasks at $0.30/1M — significant upgrade from M2.1 |
| 🎯 toolCalling Flag per Model | Per-model toolCalling: true/false in registry — AutoCombo skips non-tool-capable models |
| 🌍 Multilingual Intent Detection | PT/ZH/ES/AR keywords in AutoCombo scoring — better model selection for non-English content |
| 📊 Benchmark-Driven Fallbacks | Real p95 latency from live requests feeds combo scoring — AutoCombo learns from actual data |
| 🔁 Request Deduplication | Content-hash based dedup window — multi-agent safe, prevents duplicate charges |
| 🔌 Pluggable RouterStrategy | Extensible RouterStrategy interface — add custom routing logic as plugins |
🚀 Previous v2.0.9+ — Playground, CLI Fingerprints & ACP
| Feature | What It Does |
|---|---|
| 🎮 Model Playground | Dashboard page to test any model directly — provider/model/endpoint selectors, Monaco Editor, streaming, abort, timing |
| 🔏 CLI Fingerprint Matching | Per-provider header/body ordering to match native CLI signatures — toggle per provider in Settings > Security. Your proxy IP is preserved |
| 🤖 ACP Agents Dashboard | Debug › Agents page — grid of 14 agents with install status, version, custom agent form for any CLI tool. OpenCode users get a "Download opencode.json" button that auto-generates a ready-to-use config with all available models. |
🔧 Custom Model apiFormat Routing |
Custom models with apiFormat: "responses" now correctly route to the Responses API translator |
| 🏢 Codex Workspace Isolation | Multiple Codex workspaces per email — OAuth correctly separates connections by workspace ID |
| 🔄 Electron Auto-Update | Desktop app checks for updates + auto-install on restart |
🤖 Agent & Protocol Operations (v2.0)
| Feature | What It Does |
|---|---|
| 🔧 MCP Server (25 tools) | IDE/agent tools via 3 transports: stdio, SSE (/api/mcp/sse), Streamable HTTP (/api/mcp/stream). 18 core + 3 memory + 4 skill tools |
| 🤝 A2A Server (JSON-RPC + SSE) | Agent-to-agent task execution with sync and streaming flows |
| 🧭 Consolidated Endpoints Page | Tabbed management page with Endpoint Proxy, MCP, A2A, and API Endpoints tabs |
| 🎚️ Service Enable/Disable Toggles | ON/OFF switches for MCP and A2A with settings persistence (default: OFF) |
| 🛰️ MCP Runtime Heartbeat | Real process status (pid, uptime, heartbeat age, transport, scope mode) |
| 📋 MCP Audit Trail | Filterable audit logs with success/failure and key attribution |
| 🔐 MCP Scope Enforcement | 10 granular scope permissions for controlled tool access |
| 📡 A2A Task Lifecycle Management | List/filter tasks, inspect events/artifacts, cancel running tasks |
| 📋 Agent Card Discovery | /.well-known/agent.json for client auto-discovery |
| 🧪 Protocol E2E Test Harness | Real MCP SDK + A2A client flows in test:protocols:e2e |
| ⚙️ Operational Controls | Switch combos, tune resilience settings, and review breaker state from dedicated Health and Settings surfaces |
🧠 Routing & Intelligence
| Feature | What It Does |
|---|---|
| 🎯 Smart 4-Tier Fallback | Auto-route: Subscription → API Key → Cheap → Free |
| 📊 Real-Time Quota Tracking | Live token count + reset countdown per provider |
| 🔄 Format Translation | OpenAI ↔ Claude ↔ Gemini ↔ Responses with schema-safe conversions |
| 👥 Multi-Account Support | Multiple accounts per provider with intelligent selection |
| 🔄 Auto Token Refresh | OAuth tokens refresh automatically with retry |
| 🎨 Custom Combos | 13 balancing strategies + fallback chain control |
| 🔗 Context Relay | Session continuity handoffs when account rotation happens mid-session |
| 🌐 Wildcard Router | provider/* dynamic routing |
| 🧠 Thinking Budget Controls | Passthrough, auto, custom, and adaptive reasoning limits |
| 🔀 Model Aliases | Built-in + custom model aliasing and migration safety |
| ⚡ Background Degradation | Route low-priority background tasks to cheaper models |
| 🧪 Task-Aware Smart Routing | Auto-select model by content type (coding/vision/analysis/summarization) |
| 🔄 A2A Agent Workflows | Deterministic FSM orchestrator for stateful multi-step agent executions |
| 🔀 Adaptive Routing | Dynamic strategy override based on token volume and prompt complexity |
| 🎲 Provider Diversity | Shannon entropy scoring balancing auto-combo traffic distribution |
| 💬 System Prompt Injection | Global behavior controls applied consistently |
| 📄 Responses API Compatibility | Full /v1/responses support for Codex and advanced agentic workflows |
🎵 Multi-Modal APIs
| Feature | What It Does |
|---|---|
| 🖼️ Image Generation | /v1/images/generations with cloud and local backends |
| 📐 Embeddings | /v1/embeddings for search and RAG pipelines |
| 🎤 Audio Transcription | /v1/audio/transcriptions — 7 providers (Deepgram Nova 3, AssemblyAI, Groq Whisper, HuggingFace, ElevenLabs, OpenAI, Azure), auto-language detection, MP4/MP3/WAV support |
| 🔊 Text-to-Speech | /v1/audio/speech — 10 providers (ElevenLabs, OpenAI, Deepgram, Cartesia, PlayHT, HuggingFace, Nvidia NIM, Inworld, Coqui, Tortoise) with correct error messages |
| 🎬 Video Generation | /v1/videos/generations (ComfyUI + SD WebUI workflows) |
| 🎵 Music Generation | /v1/music/generations (ComfyUI workflows) |
| 🛡️ Moderations | /v1/moderations safety checks |
| 🔀 Reranking | /v1/rerank for relevance scoring |
| 🔍 Web Search 🆕 | /v1/search — 5 providers (Serper, Brave, Perplexity, Exa, Tavily), 6,500+ free/month, auto-failover, cache |
🛡️ Resilience, Security & Governance
| Feature | What It Does |
|---|---|
| 🔌 Provider Circuit Breakers | Provider-wide trip/recover after fallback exhaustion with configurable thresholds |
| 🔒 Daily Quota Lock 🆕 | Detects exhaustion signals and locks routing for the specific model until midnight |
| 🎯 Endpoint-Aware Models | Custom models declare supported endpoints + API format |
| 🛡️ Anti-Thundering Herd | Mutex + semaphore protections on retry/rate events |
| 🧠 Semantic + Signature Cache | Cost/latency reduction with two cache layers |
| ⚡ Request Idempotency | Duplicate protection window |
| 🔒 TLS Fingerprint Spoofing | Browser-like TLS fingerprint — reduces bot detection and account flagging |
| 🔏 CLI Fingerprint Matching | Matches native CLI request signatures — reduces ban risk while preserving proxy IP |
| 🌐 IP Filtering | Allowlist/blocklist control for exposed deployments |
| 🚦 Request Queue & Pacing | Configurable per-connection request buckets for RPM, spacing, concurrency, and max wait |
| 📉 Graceful Degradation | Multi-layer capability fallbacks protecting core gateway operations |
| 📜 Config Audit Trail | Diff-based change tracking preventing operational drift with simple rollbacks |
| ⏳ Provider Health Sync | Proactive token expiration monitoring triggering alerts before authorization failures |
| ❄️ Connection Cooldown | Retryable 408/429/5xx failures cool down a single connection with optional upstream hints |
| 🚪 Auto-Disable Banned Accounts | Permanently blocked token accounts can be disabled automatically |
| 🔑 API Key Management + Scoping | Secure key issuance/rotation and model/provider controls |
| 👁️ Scoped API Key Reveal 🆕 | Opt-in recovery of API keys via ALLOW_API_KEY_REVEAL |
🛡️ Protected /models |
Optional auth gating and provider hiding for model catalog |
| 🛡️ Safe Outbound Fetch 🆕 | Guarded fetch for provider calls — blocks private/local URLs, retries, SSRF protection |
| ⏳ Wait For Cooldown 🆕 | Auto-retry chat after connection cooldowns; configurable enabled, maxRetries, and maxRetryWaitSec |
| 🔍 Runtime Env Validation 🆕 | Zod-based env schema validation at startup with actionable error messages |
| 📋 Compliance Audit v2 🆕 | Pagination, request context, auth events, provider CRUD, and SSRF-blocked logging |
📊 Observability & Analytics
| Feature | What It Does |
|---|---|
| 📝 Request + Proxy Logging | Full request/response and proxy logging |
| 📉 Streamed Detailed Logs | Reconstructs SSE payload streams cleanly into the UI |
| 🏷️ Real-Time Model Badges 🆕 | Live model status and daily quota countdown timers |
| 📋 Unified Logs Dashboard | Request, proxy, audit, and console views in one page |
| 🔍 Request Telemetry | p50/p95/p99 latency and request tracing |
| 🏥 Health Dashboard | Uptime, breaker states, lockouts, cache stats |
| 💰 Cost Tracking | Budget controls and per-model pricing visibility |
| 📈 Analytics Visualizations | Model/provider usage insights and trend views |
| 🧪 Evaluation Framework | Golden set testing with configurable match strategies |
| 📡 Live Diagnostics 🆕 | Semantic cache bypass for accurate combo live testing |
| 🔐 TPS Log Metric 🆕 | Tokens Per Second badge in log details modal |
☁️ Deployment & Platform
| Feature | What It Does |
|---|---|
| 🌐 Deploy Anywhere | Localhost, VPS, Docker, Cloud environments |
| 🚇 Cloudflare Tunnel 🆕 | One-click Quick Tunnel integration from the dashboard |
| 🔑 API Key Model Filtering | Native /v1/models response filtered via assigned Bearer context roles |
| ⚡ Smart Cache Bypass | Configurable TTL heuristics and forced refetch controls |
| 🔄 Backup/Restore | Export/import and disaster recovery flows |
| 🧙 Onboarding Wizard | First-run guided setup |
| 🔧 CLI Tools Dashboard | One-click setup for popular coding tools |
| 🎮 Model Playground | Test any provider/model/endpoint from the dashboard |
| 🔏 CLI Fingerprint Toggle | Per-provider fingerprint matching in Settings > Security |
| 🌐 i18n (30 languages) | Full dashboard + docs language support with RTL coverage |
| 🧹 Clear All Models | One-click model list clearing in provider details |
| 👁️ Sidebar Controls 🆕 | Hide components and integrations from Appearance Settings |
| 📋 Issue Templates | Standardized GitHub templates for bugs and features |
| 📂 Custom Data Directory | DATA_DIR override for storage location |
| 🌐 V1 WebSocket Bridge 🆕 | OpenAI-compatible WebSocket traffic proxied via /v1/ws |
| 🔑 Sync Tokens & Bundle 🆕 | Config sync tokens + versioned bundle endpoint with ETag support |
Feature Deep Dive
Smart fallback with practical cost control
Combo: "my-coding-stack"
1. cc/claude-opus-4-7
2. nvidia/llama-3.3-70b
3. glm/glm-4.7
4. if/kimi-k2-thinking
When quota, rate, or health fails, OmniRoute automatically moves to the next candidate without manual switching.
Protocol management that is visible and operable
- MCP + A2A are discoverable in UI and docs (not hidden)
- Protocol status APIs expose live operational data (
/api/mcp/*,/api/a2a/*) - Dashboards include actions for day-2 ops (combo toggles, breaker resets, task cancellation)
Translator + validation workflow
The Translator area includes:
- Playground: request transformation checks
- Chat Tester: full request/response round-trip
- Test Bench: multiple cases in one run
- Live Monitor: real-time traffic view
Plus protocol validation with real clients via npm run test:protocols:e2e.
📖 MCP Server README — Tool reference, IDE configs, and client examples
📖 A2A Server README — Skills, JSON-RPC methods, streaming, and task lifecycle
🧪 Evaluations (Evals)
OmniRoute includes a built-in evaluation framework to test LLM response quality against a golden set. Access it via Analytics → Evals in the dashboard.
Built-in Golden Set
The pre-loaded "OmniRoute Golden Set" contains test cases for:
- Greetings, math, geography, code generation
- JSON format compliance, translation, markdown generation
- Safety refusal (harmful content), counting, boolean logic
Evaluation Strategies
| Strategy | Description | Example |
|---|---|---|
exact |
Output must match exactly | "4" |
contains |
Output must contain substring (case-insensitive) | "Paris" |
regex |
Output must match regex pattern | "1.*2.*3" |
custom |
Custom JS function returns true/false | (output) => output.length > 10 |
📖 Setup Guide
Protocol Setup (MCP + A2A)
🧩 MCP Setup (Model Context Protocol)
Start MCP transport in stdio mode:
omniroute --mcp
Recommended validation flow:
- Connect your MCP client over stdio.
- Run
omniroute_get_health. - Run
omniroute_list_combos. - Open
/dashboard/mcpto confirm heartbeat, activity, and audit.
Useful APIs for automation:
GET /api/mcp/statusGET /api/mcp/toolsGET /api/mcp/auditGET /api/mcp/audit/stats
🤝 A2A Setup (Agent2Agent)
Discover the agent:
curl http://localhost:20128/.well-known/agent.json
Send a task:
curl -X POST http://localhost:20128/a2a \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":"setup-a2a","method":"message/send","params":{"skill":"quota-management","messages":[{"role":"user","content":"Summarize quota status."}]}}'
Manage lifecycle:
GET /api/a2a/statusGET /api/a2a/tasksGET /api/a2a/tasks/:idPOST /api/a2a/tasks/:id/cancel
Operational UI:
/dashboard/a2afor task/state/stream observability and smoke actions
🧪 End-to-end protocol validation
Validate both protocols with real clients:
npm run test:protocols:e2e
This verifies:
- MCP SDK client connect/list/call
- A2A discovery/send/stream/get/cancel
- Cross-check data in MCP audit and A2A task management APIs
💳 Subscription Providers
Claude Code (Pro/Max)
Dashboard → Providers → Connect Claude Code
→ OAuth login → Auto token refresh
→ 5-hour + weekly quota tracking
Models:
cc/claude-opus-4-7
cc/claude-sonnet-4-5-20250929
cc/claude-haiku-4-5-20251001
Pro Tip: Use Opus for complex tasks, Sonnet for speed. OmniRoute tracks quota per model!
OpenAI Codex (Plus/Pro)
Dashboard → Providers → Connect Codex
→ OAuth login (port 1455)
→ 5-hour + weekly reset
Models:
cx/gpt-5.2-codex
cx/gpt-5.1-codex-max
Codex Account Limit Management (5h + Weekly)
Each Codex account now has policy toggles in Dashboard -> Providers:
5h(ON/OFF): enforce the 5-hour window threshold policy.Weekly(ON/OFF): enforce the weekly window threshold policy.- Threshold behavior: when an enabled window reaches >=90% usage, that account is skipped.
- Rotation behavior: OmniRoute routes to the next eligible Codex account automatically.
- Reset behavior: when the provider
resetAttime passes, the account becomes eligible again automatically.
Scenarios:
5h ON+Weekly ON: account is skipped when either window reaches threshold.5h OFF+Weekly ON: only weekly usage can block the account.5h ON+Weekly OFF: only 5-hour usage can block the account.resetAtpassed: account re-enters rotation automatically (no manual re-enable).
GitHub Copilot
Dashboard → Providers → Connect GitHub
→ OAuth via GitHub
→ Monthly reset (1st of month)
Models:
gh/gpt-5
gh/claude-4.5-sonnet
gh/gemini-3.1-pro-preview
🔑 API Key Providers
NVIDIA NIM (FREE developer access — 70+ models)
- Sign up: build.nvidia.com
- Get free API key (1000 inference credits included)
- Dashboard → Add Provider → NVIDIA NIM:
- API Key:
nvapi-your-key
- API Key:
Models: nvidia/llama-3.3-70b-instruct, nvidia/mistral-7b-instruct, and 50+ more
Pro Tip: OpenAI-compatible API — works seamlessly with OmniRoute's format translation!
DeepSeek
- Sign up: platform.deepseek.com
- Get API key
- Dashboard → Add Provider → DeepSeek
Models: deepseek/deepseek-chat, deepseek/deepseek-coder
Groq (Free Tier Available!)
- Sign up: console.groq.com
- Get API key (free tier included)
- Dashboard → Add Provider → Groq
Models: groq/llama-3.3-70b, groq/mixtral-8x7b
Pro Tip: Ultra-fast inference — best for real-time coding!
OpenRouter (100+ Models)
- Sign up: openrouter.ai
- Get API key
- Dashboard → Add Provider → OpenRouter
Models: Access 100+ models from all major providers through a single API key.
Dashboard behavior: OpenRouter models are managed from Available Models. Manual add, import, and auto-sync all update the same list.
💰 Cheap Providers (Backup)
GLM-4.7 (Daily reset, $0.6/1M)
- Sign up: Zhipu AI
- Get API key from Coding Plan
- Dashboard → Add API Key:
- Provider:
glm - API Key:
your-key
- Provider:
Use: glm/glm-4.7
Pro Tip: Coding Plan offers 3× quota at 1/7 cost! Reset daily 10:00 AM.
MiniMax M2.1 (5h reset, $0.20/1M)
- Sign up: MiniMax
- Get API key
- Dashboard → Add API Key
Use: minimax/MiniMax-M2.1
Pro Tip: Cheapest option for long context (1M tokens)!
Kimi K2 ($9/month flat)
- Subscribe: Moonshot AI
- Get API key
- Dashboard → Add API Key
Use: kimi/kimi-latest
Pro Tip: Fixed $9/month for 10M tokens = $0.90/1M effective cost!
🆓 FREE Providers (Emergency Backup)
Qoder (5 FREE models via OAuth)
Dashboard → Connect Qoder
→ Qoder OAuth login
→ Unlimited usage
Models:
if/kimi-k2-thinking
if/qwen3-coder-plus
if/glm-4.7
if/minimax-m2
if/deepseek-r1
Qwen (4 FREE models via Device Code)
Dashboard → Connect Qwen
→ Device code authorization
→ Unlimited usage
Models:
qw/qwen3-coder-plus
qw/qwen3-coder-flash
Kiro (Claude FREE)
Dashboard → Connect Kiro
→ AWS Builder ID or Google/GitHub
→ Unlimited usage
Models:
kr/claude-sonnet-4.5
kr/claude-haiku-4.5
🎨 Create Combos
Example 1: Maximize Subscription → Cheap Backup
Dashboard → Combos → Create New
Name: premium-coding
Models:
1. cc/claude-opus-4-7 (Subscription primary)
2. glm/glm-4.7 (Cheap backup, $0.6/1M)
3. minimax/MiniMax-M2.1 (Cheapest fallback, $0.20/1M)
Use in CLI: premium-coding
Example 2: Free-Only (Zero Cost)
Name: free-combo
Models:
1. if/kimi-k2-thinking (unlimited)
2. qw/qwen3-coder-plus (unlimited)
Cost: $0 forever!
🔧 CLI Integration
Cursor IDE
Settings → Models → Advanced:
OpenAI API Base URL: http://localhost:20128/v1
OpenAI API Key: [from OmniRoute dashboard]
Model: cc/claude-opus-4-7
Claude Code
Use the CLI Tools page in the dashboard for one-click configuration, or edit ~/.claude/settings.json manually.
Codex CLI
export OPENAI_BASE_URL="http://localhost:20128"
export OPENAI_API_KEY="your-omniroute-api-key"
codex "your prompt"
OpenClaw
Option 1 — Dashboard (recommended):
Dashboard → CLI Tools → OpenClaw → Select Model → Apply
Option 2 — Manual: Edit ~/.openclaw/openclaw.json:
{
"models": {
"providers": {
"omniroute": {
"baseUrl": "http://127.0.0.1:20128/v1",
"apiKey": "sk_omniroute",
"api": "openai-completions"
}
}
}
}
Note: OpenClaw only works with local OmniRoute. Use
127.0.0.1instead oflocalhostto avoid IPv6 resolution issues.
Cline / Continue / RooCode
Settings → API Configuration:
Provider: OpenAI Compatible
Base URL: http://localhost:20128/v1
API Key: [from OmniRoute dashboard]
Model: if/kimi-k2-thinking
OpenCode
Step 1: Add OmniRoute as a custom provider:
opencode
/connect
# Select "Other" → Enter ID: "omniroute" → Enter your OmniRoute API key
Step 2: Create/edit opencode.json in your project root:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"omniroute": {
"npm": "@ai-sdk/openai-compatible",
"name": "OmniRoute",
"options": {
"baseURL": "http://localhost:20128/v1"
},
"models": {
"cc/claude-sonnet-4-20250514": { "name": "Claude Sonnet 4" },
"gg/gemini-2.5-pro": { "name": "Gemini 2.5 Pro" },
"if/kimi-k2-thinking": { "name": "Kimi K2 (Free)" }
}
}
}
}
Step 3: Select the model in OpenCode:
/models
# Select any OmniRoute model from the list
Tip: Add any model available in your OmniRoute
/v1/modelsendpoint to themodelssection. Use the formatprovider/model-idfrom your OmniRoute dashboard.
समस्या निवारण
Click to expand troubleshooting guide
"Language model did not provide messages"
- Provider quota exhausted → Check dashboard quota tracker
- Solution: Use combo fallback or switch to cheaper tier
Rate limiting
- Subscription quota out → Fallback to GLM/MiniMax
- Add combo:
cc/claude-opus-4-7 → glm/glm-4.7 → if/kimi-k2-thinking
OAuth token expired
- Auto-refreshed by OmniRoute
- If issues persist: Dashboard → Provider → Reconnect
High costs
- Check usage stats in Dashboard → Costs
- Switch primary model to GLM/MiniMax
Dashboard/API ports are wrong
PORTis the canonical base port (and API port by default)API_PORToverrides only OpenAI-compatible API listenerDASHBOARD_PORToverrides only dashboard/Next.js listener- Set
NEXT_PUBLIC_BASE_URLto your dashboard/public URL (for OAuth callbacks)
Cloud sync errors
- Verify
BASE_URLpoints to your running instance - Verify
CLOUD_URLpoints to your expected cloud endpoint - Keep
NEXT_PUBLIC_*values aligned with server-side values
First login not working
- Check
INITIAL_PASSWORDin.env - If unset, fallback password is
123456
No request logs
call_logsin SQLite stores summary metadata for the Request Logs table and analytics views- Detailed request/response payloads are written to
DATA_DIR/call_logs/as one JSON artifact per request - Enable pipeline capture from Dashboard → Logs → Request Logs if you need detailed per-stage payloads
Export Logsreads the artifact files on demand, whileExport Allincludes thecall_logs/directory alongsidestorage.sqlite- Set
APP_LOG_TO_FILE=trueif you also want application console logs inlogs/application/app.log - Adjust
APP_LOG_MAX_FILE_SIZE,APP_LOG_RETENTION_DAYS,APP_LOG_MAX_FILES, andCALL_LOG_MAX_ENTRIESas needed
Connection test shows "Invalid" for OpenAI-compatible providers
- Many providers don't expose a
/modelsendpoint - OmniRoute v1.0.6+ includes fallback validation via chat completions
- Ensure base URL includes
/v1suffix
🔐 OAuth on a Remote Server
⚠️ Important for users running OmniRoute on a VPS, Docker, or any remote server
The OAuth credentials bundled in OmniRoute are registered for localhost only. When you access OmniRoute on a remote server (e.g. https://omniroute.myserver.com), Google rejects the authentication with:
Error 400: redirect_uri_mismatch
Solution: Configure your own OAuth credentials
You need to create an OAuth 2.0 Client ID in Google Cloud Console with your server's URI.
Step-by-step
1. Open Google Cloud Console
Go to: https://console.cloud.google.com/apis/credentials
2. Create a new OAuth 2.0 Client ID
- Click "+ Create Credentials" → "OAuth client ID"
- Application type: "Web application"
- Name: anything you like (e.g.
OmniRoute Remote)
3. Add Authorized Redirect URIs
In the "Authorized redirect URIs" field, add:
https://your-server.com/callback
Replace
your-server.comwith your server's domain or IP (include the port if needed, e.g.http://45.33.32.156:20128/callback).
4. Save and copy the credentials
After creating, Google will show the Client ID and Client Secret.
5. Set environment variables
In your .env (or Docker environment variables):
# For Antigravity:
ANTIGRAVITY_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com
ANTIGRAVITY_OAUTH_CLIENT_SECRET=GOCSPX-your-secret
GEMINI_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com
GEMINI_OAUTH_CLIENT_SECRET=GOCSPX-your-secret
6. Restart OmniRoute
# npm:
npm run dev
# Docker:
docker restart omniroute
7. Try connecting again
Google will now redirect correctly to https://your-server.com/callback.
Temporary workaround (without custom credentials)
If you don't want to set up your own credentials right now, you can still use the manual URL flow:
- OmniRoute opens the Google authorization URL
- After authorizing, Google tries to redirect to
localhost(which fails on the remote server) - Copy the full URL from your browser's address bar (even if the page doesn't load)
- Paste that URL into the field shown in the OmniRoute connection modal
- Click "Connect"
This works because the authorization code in the URL is valid regardless of whether the redirect page loaded.
🛠️ Tech Stack
Click to expand tech stack details
- Runtime: Node.js 18–22 LTS (⚠️ Node.js 24+ is not supported —
better-sqlite3native binaries are incompatible) - Language: TypeScript 5.9 — 100% TypeScript across
src/andopen-sse/(zeroanyin core modules since v2.0) - Framework: Next.js 16 + React 19 + Tailwind CSS 4
- Database: better-sqlite3 (SQLite) + LowDB (JSON legacy) — domain state, proxy logs, MCP audit, routing decisions, memory, skills
- Schemas: Zod (MCP tool I/O validation, API contracts)
- Protocols: MCP (stdio/HTTP) + A2A v0.3 (JSON-RPC 2.0 + SSE)
- Streaming: Server-Sent Events (SSE)
- Auth: OAuth 2.0 (PKCE) + JWT + API Keys + MCP Scoped Authorization
- Testing: Node.js test runner + Vitest (900+ tests including unit, integration, E2E)
- CI/CD: GitHub Actions (auto npm publish + Docker Hub on release)
- Website: omniroute.online
- Package: npmjs.com/package/omniroute
- Docker: hub.docker.com/r/diegosouzapw/omniroute
- Resilience: Circuit breaker, exponential backoff, anti-thundering herd, TLS spoofing, auto-combo self-healing
दस्तावेज़
| Document | Description |
|---|---|
| User Guide | Providers, combos, CLI integration, deployment |
| API Reference | All endpoints with examples |
| MCP Server | 25 MCP tools, IDE configs, Python/TS/Go clients |
| A2A Server | JSON-RPC 2.0 protocol, skills, streaming, task mgmt |
| Auto-Combo Engine | 6-factor scoring, mode packs, self-healing |
| Context Relay | Session handoff strategy for account rotation |
| Troubleshooting | Common problems and solutions |
| Architecture | System architecture and internals |
| Codebase Documentation | Beginner-friendly codebase walkthrough |
| Uninstall Guide | Clean removal for all install methods |
| Environment Config | Complete .env variables and references |
| Contributing | Development setup and guidelines |
| OpenAPI Spec | OpenAPI 3.0 specification |
| Security Policy | Vulnerability reporting and security practices |
| VM Deployment | Complete guide: VM + nginx + Cloudflare setup |
| Features Gallery | Visual dashboard tour with screenshots |
| Release Checklist | Pre-release validation steps |
🗺️ Roadmap
OmniRoute has 218+ features planned across multiple development phases. Here are the key areas:
| Category | Planned Features | Highlights |
|---|---|---|
| 🧠 Routing & Intelligence | 25+ | Lowest-latency routing, tag-based routing, quota preflight, quota-aware P2C, step-based combo routing |
| 🔒 Security & Compliance | 20+ | SSRF hardening, credential cloaking, rate-limit per endpoint, management key scoping |
| 📊 Observability | 15+ | OpenTelemetry integration, real-time quota monitoring, combo target health, cost tracking per model |
| 🔄 Provider Integrations | 20+ | Dynamic model registry, connection cooldowns, multi-account Codex, Copilot quota parsing |
| ⚡ Performance | 15+ | Dual cache layer, prompt cache, response cache, streaming keepalive, batch API |
| 🌐 Ecosystem | 10+ | WebSocket API, config hot-reload, distributed config store, commercial mode |
🔜 Coming Soon
- 🔗 OpenCode Integration — Native provider support for the OpenCode AI coding IDE
- 🔗 TRAE Integration — Full support for the TRAE AI development framework
- 📦 Batch API — Asynchronous batch processing for bulk requests
- 🎯 Tag-Based Routing — Route requests based on custom tags and metadata
- 💰 Lowest-Cost Strategy — Automatically select the cheapest available provider
📝 Full feature specifications available in
docs/new-features/(217 detailed specs)
👥 Contributors
How to Contribute
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
See CONTRIBUTING.md for detailed guidelines.
Releasing a New Version
# Create a release — npm publish happens automatically
gh release create v2.0.0 --title "v2.0.0" --generate-notes
📊 Star History
🌍 StarMapper
🙏 Acknowledgments
Special thanks to CLIProxyAPI — the original Go implementation that inspired this JavaScript port.
लाइसेंस
MIT License - see LICENSE for details.









