Skip to content

v3.8.47

Choose a tag to compare

@diegosouzapw diegosouzapw released this 13 Jul 14:57
e8950de
  • 9router Codex import: the Codex bulk-import endpoint (POST /api/oauth/codex/import) now accepts 9router's camelCase account export (accessToken/refreshToken/idToken/expiresAt + nested providerSpecificData), not just snake_case — normalizeCodexImportRecord maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged (#6665) — thanks @deadcoder0904. Regression guard: tests/unit/codexBulkImport.test.ts (9router camelCase record, pre-supplied providerSpecificData without an id_token, snake_case-not-overridden, and a full {accounts:[...]} flatten).

✨ New Features

  • feat(plugins): Langfuse observability plugin. (#6577 — thanks @chirag127)

  • feat(combo): context requirements config for per-target filtering in combos. (#6907 — thanks @oyi77)

  • feat(providers): icons for 46 providers that were missing images. (#6926 — thanks @oyi77)

  • feat(compression): vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). (#6838 — thanks @blackwell-systems)

  • feat(proxy): shorthand proxy formats + protocol header mode for bulk import. (#6867 — thanks @growab)

  • feat(provider): OpenVecta AI inference gateway. (#6833 — thanks @hajilok)

  • feat(i18n): Traditional Chinese (zh-TW) localization for frontend and CLI. (#6320 — thanks @lunkerchen)

  • feat(xai): route xAI clients to Grok's native /v1/responses endpoint. (#6709 — thanks @diegosouzapw)

  • feat(routing): per-model web-search/web-fetch interception rules. (#3384, #6814 — thanks @diegosouzapw)

  • feat(release): changelog.d/ fragments — eliminates the CHANGELOG merge-storm cascade. (#6783 — thanks @diegosouzapw)

  • feat(quality): validate-release-green --full-ci reproduces the entire ci.yml static gate set locally. (#6583 — thanks @diegosouzapw)

  • feat(dashboard): sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (src/shared/components/Sidebar.tsx) filters nav sections/groups/items client-side by label as you type, reusing the existing common.search/common.noResults i18n keys (zero new locale edits) and the shared Input icon="search" pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into filterSidebarSectionsByQuery() (src/shared/utils/sidebarSearch.ts) for isolated unit testing. Regression guard: tests/unit/sidebar-search-filter.test.ts, src/shared/components/Sidebar.search.test.tsx. (#4013 — thanks @crochabe-cyber)

  • feat(combo): auto/* combos gain a strict budget-cap fallback policy — X-OmniRoute-Budget-Fallback: strict (or the persisted config.budgetFallback: "strict") makes an over-budget request fail fast with HTTP 402 instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (cheapest) preserves existing behavior. Builds on the existing X-OmniRoute-Budget/X-OmniRoute-Mode per-request controls (#6023/#6024/#6025), consolidated into resolveRequestAutoControls(). Regression guard: tests/unit/auto-combo-budget-fallback-3470.test.ts. (#3470)

  • Provider/model param filters: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625)

  • Per-combo reasoning token buffer toggle: the combo builder now exposes an explicit checkbox for the #3587 reasoning-model max_tokens buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev)

  • feat(dashboard): 9router-parity Routing Strategy settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page (#6678) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (comboStickyRoundRobinLimit, resolved via resolveComboStickyRoundRobinLimit() — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new providerStrategies setting (Zod-validated map, src/shared/validation/settingsSchemas.ts) lets a specific provider override the global fallbackStrategy/stickyRoundRobinLimit without touching the account-wide default, wired into getProviderCredentials() (src/sse/services/auth.ts) ahead of the global fallback. Regression guard: tests/unit/combo-rr-sticky-9router.test.ts, tests/unit/settings-ui-layout-static.test.ts. (thanks @SeaXen)

  • feat(icons): provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → @lobehub/icons React components → thesvg.org CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: tests/unit/ui/ProviderIcon-icon-url.test.tsx. (#6317 — thanks @hamsa0x7)

  • Skill Collector CLI detection: new GET /api/skills/collect/detect + POST /api/skills/collect/install (and the cli-skill-collector agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via getCliRuntimeStatus(), match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and GET/POST /api/github-skills now require management auth (requireManagementAuth()) and are loopback-gated (LOCAL_ONLY_API_PREFIXES + SPAWN_CAPABLE_PREFIXES) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The omniroute_github_skills_install MCP tool now reports the honest action: "planned" instead of "installed", matching the REST route (#6294 — thanks @Moseyuh333)

  • ClinePass dual-auth: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to cp (matching the OAUTH_PROVIDERS catalog alias) so <alias>/<modelId> routing resolves correctly, the OAuth refresh dispatch now routes clinepass to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: tests/unit/clinepass-provider.test.ts. (#6126 — thanks @hajilok)

  • feat(oauth): Kiro/Amazon Q auto-import now supports enterprise External IdP ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no aorAAAAAG-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so tryAwsSsoCache() now detects them (authMethod/provider === "externalidp") and refreshes via the org IdP's own tokenEndpoint (public-client OAuth2 refresh grant, no client secret), persisting TokenType: EXTERNAL_IDP gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; tokenEndpoint is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc)

  • Kiro long-lived API key auth: new /api/oauth/kiro/api-key route + KiroService.validateApiKey let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (ListAvailableModels, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp)

  • Chaos Mode: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (GET/PUT/DELETE /api/chaos/config) and gated per-API-key via a new chaosModeEnabled permission (opt-in — disabled by default globally and per key). POST /api/chaos/run (dashboard session) and POST /api/skills/collect/chaos (external Bearer-token) delegate to a shared executeChaosRun() engine (src/lib/chaos/chaosExecutor.ts) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable max_tokens (256–128k), a clear error when stream is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: tests/unit/chaos-config.test.ts, tests/unit/chaos-executor.test.ts, tests/unit/chaos-api-routes.test.ts. (#6728 — thanks @Moseyuh333)

  • feat(cli): 2 new CLI tool integrations on Dashboard → CLI Tools — omp (Oh My Pi) and letta — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to which omp/which letta to detect the local install, so they're loopback-gated (LOCAL_ONLY_API_PREFIXES, Hard Rules #15/#17) in addition to the shared requireCliToolsAuth() management-auth guard every cli-tools route requires, and route errors through sanitizeErrorMessage(); src/lib/db/omp.ts isolates the omp CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed pi, codewhale, and jcode integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: tests/unit/db/omp.test.ts, tests/unit/cli-tools-auth-hardening.test.ts, tests/integration/cli-settings-omp.test.ts, tests/integration/cli-settings-letta.test.ts. (#6318 — thanks @hamsa0x7)

  • feat(providers): custom models now support a manual 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 silently getting dropped from combo routing once the wrong value lands in the catalog (#4125 — thanks @rucciva). 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), GET surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: tests/unit/provider-models-context-window-override-4125.test.ts.

  • fix(providers): register OpenRouter as a rerank provider so openrouter/cohere/rerank-* models resolve instead of erroring Invalid rerank model (#6574 — thanks @rafpigna)

  • fix(api): HEAD requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed (#6400), broader follow-up to the route-specific #6517 (/v1/models). Root cause: Next.js 16's App Router route-handler pipeline (next/dist/server/send-response.js) correctly skips piping a Response body for HEAD, but its page-rendering pipeline (next/dist/server/pipe-readable.jspipeToNodeResponse, used for every app-router page/layout render — including the not-found boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) HEAD response had actually finished. A new scripts/dev/head-response-guard.cjs, wired into both the dev/start custom server (scripts/dev/run-next.mjs) and the packaged standalone server (scripts/dev/standalone-server-ws.mjs) at the same tier as the existing http-method-guard.cjs/peer-stamp.mjs wrappers, discards any body bytes written for a HEAD request and forces Connection: close once .end() is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: tests/unit/head-request-closes-6400.test.ts.

  • feat(dashboard): Provider Quota page (Dashboard → Quota) fills horizontal whitespace before stacking vertically (#3520) — QuotaCardGrid previously stacked every provider group in a single vertical flex flex-col, and each group's own card grid didn't go multi-column until md (grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4). 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 (grid-cols-2 md:grid-cols-3 xl:grid-cols-4), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: tests/unit/quota-card-grid-horizontal-layout.test.ts (thanks @gdevenyi).

  • feat(ws): the live-dashboard WebSocket server now auto-starts in-process (via instrumentation-node.ts) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to 20132 to avoid colliding with API_PORT in split-port setups, the deprecated OMNIROUTE_DISABLE_LIVE_WS env was consolidated into OMNIROUTE_ENABLE_LIVE_WS (default enabled), and the WS path is now derived from NEXT_PUBLIC_LIVE_WS_PUBLIC_URL's pathname (/live-ws fallback) (#6072 — thanks @ianriizky).

  • feat(compression): new omniglyph engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (stackPriority: 90) or standalone (mode: omniglyph); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with skip:<reason> techniques, preview (stable: false, off by default) (#6556). Dependency bumped to omniglyph@^1.0.2 for upstream ReDoS fixes (#6661).

  • feat(sandbox): the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (wslc.exe), OrbStack, Podman — instead of hardcoding docker run, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari).

  • fix(sse): skip thinkingConfig for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211)

  • feat(xai): route xAI clients to Grok's native /v1/responses endpoint instead of the chat-completions bridge. (thanks @ryanngit)

  • feat(models): add a Settings → AI "Model Overrides" UI plus /api/model-capability-overrides CRUD and a model_capability_overrides table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev).

  • feat(resilience): operator-configurable account rotation policy — a new rotationConfig layer lets operators tune how connections rotate on failure, wired into accountFallback (#6763 — thanks @artickc).

  • chore(cursor): add Grok 4.5 effort/fast model IDs (#6774 — thanks @andrewmunsell).

  • feat(codex): Codex provider model discovery now fetches the live catalog from chatgpt.com/backend-api/codex/models using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new src/app/api/providers/[id]/models/discovery/codex.ts (normalization, version-gating, merge/enrich against the local catalog) covered by tests/unit/provider-models-discovery-split.test.ts and tests/unit/provider-models-route-codex.test.ts (#6776 — thanks @JxnLexn).

  • feat(cursor): register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell).

  • Changelog fragments (changelog.d/): PRs now add their changelog entry as a new fragment file (changelog.d/{features|fixes|maintenance}/<PR>-<slug>.md) instead of editing CHANGELOG.md — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). scripts/release/aggregate-changelog.mjs (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and check:changelog-integrity now also validates fragment well-formedness. Regression guard: tests/unit/changelog-fragments.test.ts.

  • feat(proxy): add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan).

  • feat(fusion): the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127).

  • feat(dashboard): search box on the Playground's raw model <select> (#4086) — the shared ModelSelectModal (combo builder + CLI-code cards) already had search, but Playground's StudioConfigPane model dropdown stayed a flat unsearchable list, unusable once a provider like OpenRouter contributed 50+ models. Typing now filters the dropdown (Turkish-safe accent/case-insensitive match via matchesSearch), while the currently selected model always stays pinned in the list even if it no longer matches 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 translation key needed. Regression guard: tests/unit/playground-model-selection-3731.test.ts (filterModelsByQuery), tests/unit/ui/playground-model-search-4086.test.tsx.

  • feat(usage): Antigravity/agy quota widget now surfaces the weekly window alongside the existing per-model 5-hour window (#4017) — the weekly limit isn't part of the per-model retrieveUserQuota response the fetcher already calls; it only appears in a separate, undocumented retrieveUserQuotaSummary RPC that groups models into families ("Gemini Models", "Claude and GPT models") with one weekly bucket per family. A new usage/antigravityWeeklyQuota.ts leaf fetches that RPC (cached, best-effort — a failure or unavailable RPC never breaks the existing per-model quotas) and parses the weekly bucket per group into gemini_weekly/claude_gpt_weekly quota entries, merged into the same quotas map the widget already renders generically. Regression guard: tests/unit/antigravity-weekly-quota-4017.test.ts (bucket parsing, the alternate quotaSummary-nested envelope, and end-to-end merge via getUsageForProvider).

  • feat(codex): 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 (#3697). openaiToOpenAIResponsesResponse (open-sse/translator/response/openai-responses.ts) now threads the upstream model into the Responses event objects; a new isCodexOriginatedHeaders() (open-sse/config/codexIdentity.ts, reusing PR #3481's originator/User-Agent detection) makes chatCore's existing opt-in echoRequestedModelName (#1311) model-echo pipeline fire automatically for Codex clients regardless of the setting, detected by request headers so it still applies when codex/gpt-5.5-xhigh is routed through a combo to a non-codex upstream; echoModelInObject/echoModelInSseLine (open-sse/services/responseModelEcho.ts) now also rewrite the nested response.model field the Responses API uses. /v1/models still returns models: [] for Codex (unchanged). Regression guard: tests/unit/codex-effort-model-echo-3697.test.ts.

  • Z.ai Web (free web-session provider): new zai-web web-cookie provider drives the free chat.z.ai consumer chat UI via a pasted browser session cookie, distinct from the existing API-key zai/glm/glm-cn/glmt providers (api.z.ai) — modeled on the doubao-web/venice-web cookie executors and the pre-existing chatglm-web credential requirement/token-extraction entries. ZaiWebExecutor (open-sse/executors/zai-web.ts) posts to chat.z.ai/api/chat/completions with the cookie forwarded both as Cookie and as 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 (zai-web entry, GLM-4.6/4.5/4.5V models), and tokenExtractionConfig.ts for in-app cookie capture. Regression guard: tests/unit/executor-zai-web.test.ts (16 tests — token extraction, frame parsing for both SSE shapes, streaming and non-streaming aggregation, error paths). (#4056)

  • feat(compression): update the vendored GCF codec behind the Headroom engine to spec v3.2 (nested flattening) (#6837). Homogeneous arrays whose rows carry nested objects/arrays now tabularize via >-prefixed path fields instead of a low-yield per-row fallback, so nested MCP tool-result rows (meta:{...}, tags:[...]) compact like flat rows. On representative shapes the update takes deeply-nested payloads the old codec left near-uncompressed from ~3% to ~32% vs JSON (k8s pods, cl100k_base), with shallow-nested rows seeing a small bump and flat arrays unchanged. Re-vendored from current gcf-typescript (zero runtime deps, MIT, SPDX-marked, generic-profile only); also folds in the [N]: inline-array quoting fix and canonical decimal formatting. Round-trip stays lossless (order-insensitive), and the decoder is hardened against prototype pollution (a __proto__/constructor path segment never mutates Object.prototype, and keys shadowing built-ins like toString now round-trip correctly instead of misparsing). Regression guard: tests/unit/compression/headroom-smartcrusher.test.ts (deep-nested + prototype-pollution cases).

  • feat(providers): Add GPT-5.6 support across OpenAI API, Codex, and ChatGPT Web, including Codex Max/Ultra efforts, VS Code metadata, Fast-tier credit accounting, curated live discovery, the Codex 0.144.1 client identity, and correct chat routing for models that also support image generation (#6862) - thanks @backryun

  • chore(providers): Align emitted Claude Code identity headers, bridge fingerprints, provider profiles, and documented defaults with claude-cli 2.1.207 (#6862) - thanks @backryun

🐛 Bug Fixes

  • fix(dashboard): the Proxy Registry settings page crashed at runtime (ReferenceError: poolLoaded/bulkImportOpen is not defined) — the #6625/#6909 hook-extraction refactors deleted 10 state declarations (poolLoaded, poolSaving, and the 8-member bulk-import family) while ~30 usages remained; all restored (caught by the release E2E; typecheck:core does not cover dashboard TSX — follow-up #7021). (thanks @diegosouzapw)

  • fix(combo): comboStickyRoundRobinLimit now defaults to inherit (null) instead of 1 — the literal default silently shadowed the documented batched round-robin rotation (stickyRoundRobinLimit: 3), flipping every round-robin combo to per-request alternation (#6678 follow-up, caught by the release CI). (thanks @diegosouzapw)

  • fix(ws): the standalone LiveWS startup script exited 0 without ever listening — its bootstrapped child re-spawned with the import-suppressor OMNIROUTE_ENABLE_LIVE_WS=0 and then honored it as an operator disable (#6072 follow-up, caught by the release CI). (thanks @diegosouzapw)

  • fix(api): compression PUT schema accepts every catalog engine. (#6792 — thanks @Pitchfork-and-Torch)

  • fix(compression): surface fallback reasons in the preview response. (#6461, #6519 — thanks @chirag127)

  • fix(providers): fail fast on an empty auto-combo pool instead of a 15s timeout. (#6458, #6546 — thanks @chirag127)

  • fix(compression): honor UI-toggled engines in the stackedPipeline dispatch + surface substitutions. (#6463, #6534 — thanks @chirag127)

  • fix(api): return 400 for missing/invalid messages before model resolution. (#6402, #6515 — thanks @chirag127)

  • fix(providers): enrich the model_cooldown 429 body with a retry_after ISO timestamp + credential count. (#6460, #6523 — thanks @chirag127)

  • fix(sse): default reasoning summary for effort-only Responses requests. (#6807 — thanks @rushsinging)

  • fix(sse): compression no-op treated as zero-savings, not inflation/silent-drop. (#6883 — thanks @chirag127)

  • fix(oauth): Trae OAuth client_id embedded via resolvePublicCred() (Hard Rule #11). (#6870 — thanks @chirag127)

  • fix(sse): combo path no longer trips the whole-provider breaker on a plain 429. (#6868 — thanks @chirag127)

  • fix(api): malformed JSON bodies now return 400 instead of 500. (#6871 — thanks @chirag127)

  • fix(fusion): judge selected from a surviving panel member when no explicit judge is configured. (#6869 — thanks @chirag127)

  • fix(sse): combo model lockout honors the parsed upstream quota reset. (#6863, #6866 — thanks @AgentKiller45)

  • fix(dashboard): logs detail modal no longer reopens on first close. (#6830 — thanks @MikeTuev)

  • fix(usage): honor xAI provider-reported exact cost. (#6711 — thanks @diegosouzapw)

  • fix(kiro): probe IdC region during profileArn discovery, cross-region (recovers #6099). (#6840 — thanks @diegosouzapw)

  • fix(antigravity): sanitize Cloud Code safety settings. (#6839 — thanks @diegosouzapw)

  • fix(translator): defer content_block_start until GLM streams the tool name. (#6730 — thanks @diegosouzapw)

  • fix(translator): strip empty cloud_base_branch from Cursor Subagent tool calls. (#6729 — thanks @diegosouzapw)

  • fix(antigravity): surface aborted Gemini tool calls off end_turn. (#6713 — thanks @diegosouzapw)

  • fix(volcengine): clamp Kimi max_tokens to the Ark endpoint cap. (#6712 — thanks @diegosouzapw)

  • fix(codex): surface capacity errors embedded in 200-OK SSE streams. (#6710 — thanks @diegosouzapw)

  • fix(sse): skip thinkingConfig for gemma models in openai→gemini translation. (#6708 — thanks @diegosouzapw)

  • fix(oauth): avoid bare-email dedup of Codex OAuth logins. (#6706 — thanks @diegosouzapw)

  • fix(sse): unwrap bare {function:{…}} tools in openai→claude translation. (#6704 — thanks @diegosouzapw)

  • fix(db): eliminate a redundant getApiKeyMetadata call in the embeddings route. (#6929 — thanks @oyi77)

  • fix(db): authType filter support in getProviderConnections. (#6946 — thanks @oyi77)

  • fix(i18n): the provider-detail (/dashboard/providers/[id]) visibility + free/paid model filter labels (showVisibleOnly, showHiddenOnly, freeFilterAll, freeFilterFreeOnly, freeFilterPaidOnly, hideAllModels, plus the currently-unused filterVisible/filterHidden/filterByVisibility) rendered as the literal __MISSING__:<english> sentinel in 15 locales, including pt-BR (#6694) — providerText() (providerPageHelpers.ts) checks t.has(key) before falling back to clean English, and t.has() returns true even when the stored value is the __MISSING__: sentinel scripts/i18n/sync-ui-keys.mjs writes when mirroring keys across locales, so the sentinel rendered verbatim instead of the fallback. Disjoint key set from #6290 (filterAll/filterActive/filterError/filterBanned/filterCreditsExhausted). All 9 keys now carry real translations across the 15 affected locale files (it, ja, ko, mr, ms, nl, no, phi, pl, pt, pt-BR, ro, ru, sk, sv). Regression guard: tests/unit/i18n-provider-visibility-filter-keys-6694.test.ts.

  • fix(cli): the dashboard's Claude Code CLI card could report "Not detected"/"Not installed" even when Claude Code was genuinely installed and previously used (#6701) — getCliRuntimeStatus() (src/shared/services/cliRuntime.ts) determined installed purely from binary resolution (known install paths + a where/which PATH search), with no fallback when that lookup fails for reasons unrelated to whether the CLI is actually installed (stale PATH inherited by a long-running/background process, the binary having moved, an install method not yet catalogued, etc.) — even though ~/.claude/settings.json on disk proves the tool was installed and used before. Upstream 9router's equivalent route already has this exact fallback. A new withSettingsFallback() (src/shared/services/cliInstallFallback.ts) restores 9router parity: 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, installed now reports true. Regression guard: tests/unit/repro-6701-claude-detect-fallback.test.ts.

  • fix(cli): per-agent AgentBridge DNS toggle was broken for 8 of the 9 supported agents, and a failed MITM startup step could orphan the spawned proxy child — addDNSEntry/removeDNSEntry (src/mitm/dns/dnsConfig.ts) always resolved the legacy Antigravity default hosts regardless of which agent's toggle was flipped, so enabling DNS for Cursor/Codex/Claude Code/etc. silently added only daily-cloudcode-pa.googleapis.com while the DB recorded dns_enabled=true for the selected agent. Both functions now accept an optional agentId and resolve hosts via ALL_TARGETS; POST /api/tools/agent-bridge/agents/[id]/dns passes the route's id through and now returns 404 for an id that doesn't match a known target instead of silently falling back. Separately, startMitmInternal() (src/mitm/manager.ts) now wraps generateCert() (log + rethrow), the provisionDnsEntries() call, and the PID-file write in try/catch so a mid-startup failure can't orphan the already-spawned MITM child process. On Windows, addDNSEntries/removeDNSEntries also batch every missing/present entry into a single elevated PowerShell invocation instead of one UAC prompt per host line. Regression guard: tests/unit/dns-config-generic.test.ts (agent-specific resolution + batching), tests/unit/agent-bridge-dns-route-validation.test.ts (404 for unknown agent id). (#6338 — thanks @hamsa0x7)

  • fix(guardrails): Vision Bridge's individual-model auto-reroute (route an image-bearing request straight to a vision-capable model instead of describe-then-forward) could bypass a policy-restricted API key's model allowlist/budget (#6640) — VisionBridgeGuardrail.preCall() (src/lib/guardrails/visionBridge.ts) swaps body.model to the best available vision-capable model, but that swap happens in the guardrail pipeline AFTER chat.ts already called enforceApiKeyPolicy() against the ORIGINAL model, so a key scoped to a narrow allowedModels list could still execute against an unvetted (and possibly costlier) vision model the reroute picked. chat.ts now re-validates any guardrail-driven model change against the same per-key allowlist (isModelAllowedForKey) before honoring it, falling back to the original already-approved model when the reroute target is not allowed. The reroute path also now honors an explicit settings.visionBridgeModel operator override (previously ignored, unlike the combo/describe path a few lines below it, which already respects it via getVisionBridgeConfig). Regression guard: tests/unit/guardrails/visionBridge.test.ts (22 tests). (thanks @herjarsa)

  • fix(auth): an API key restricted via allowedModels/allowedCombos could bypass that restriction entirely over the Codex Responses-over-WebSocket bridge (#6564) — prepare() in src/app/api/internal/codex-responses-ws/route.ts authenticated the WS bridge's API key (authenticate()/authorizeWebSocketHandshake()) and honored allowedConnections, but never called enforceApiKeyPolicy(), the same model/combo policy gate the HTTP /v1/responses path enforces via handleChat() — so a key scoped to e.g. combo/model-1.0 could still reach a direct Codex model like gpt-5.5 through this transport, as long as an eligible Codex OAuth connection existed. The bridge's WS auth token arrives via query params (api_key/token/access_token), not a normal Authorization header, so a new enforceCodexWsApiKeyPolicy() builds an equivalent Request carrying an explicit Authorization: Bearer <apiKey> header and calls enforceApiKeyPolicy() against the CLIENT-requested model, before any Codex-specific model remapping or credential selection. Regression guard: tests/unit/codex-ws-policy-enforcement-6564.test.ts (a model-restricted key is rejected 403 before reaching credential selection; a combo-restricted key is rejected 403 requesting a disallowed combo; a key that DOES allow the requested model still proceeds past policy). (thanks @Squawk7777 for the report and an independent fix via #6565)

  • fix(security): loopback-gate /api/middleware/* so a leaked JWT over a tunnel can't install or trigger a middleware hook — middleware hooks compile + run arbitrary JS via new vm.Script on the request hot path (src/lib/middleware/registry.ts), the same RCE class as the already-gated /api/plugins/*; /api/middleware/ is now in LOCAL_ONLY_API_PREFIXES so loopback enforcement runs unconditionally before any auth check (Hard Rules #15 + #17). Regression guard: tests/unit/route-guard-middleware-local-only.test.ts. (#6541) — see PR. (thanks @developerjillur)

  • fix(startup): AgentBridge's MITM server no longer fails to start with ROUTER_API_KEY is required on a normal install (#6403) — POST /api/tools/agent-bridge/server resolved the spawned MITM child's router key from only an explicit apiKey body field (never sent by the AgentBridge UI — the schema has no such field) and the ROUTER_API_KEY env var (unset by default), so startMitm() always received "" and the child hard-exited, even though OmniRoute already had a usable API key in its own DB. A new resolveRouterApiKey() now falls back to pickApiKeyForInternalUse() (the same DB-backed selector the combo-health-check / cloud-sync internal probes use), resolving in order: explicit key → ROUTER_API_KEY env → an existing DB key. Regression guard: tests/unit/agentbridge-mitm-router-key-6403.test.ts.

  • fix(providers): deploying a Cloudflare relay Worker from Dashboard → System → Proxy pool → Cloudflare relay failed immediately with Cloudflare Worker upload failed: Content-Type must be one of: application/javascript, text/javascript, multipart/form-data, even with a valid token/account (#6416) — the Worker-script upload built a native FormData and let fetch derive the multipart Content-Type automatically, but in production globalThis.fetch is patched with node_modules/undici's own fetch (open-sse/utils/proxyFetch.ts), whose FormData/Request classes differ from the runtime's global FormData (same cross-realm class mismatch already fixed once for image edits in #3273); passing a native FormData instance through undici's patched fetch made it serialize the body as the literal string "[object FormData]" with Content-Type: text/plain;charset=UTF-8, which Cloudflare rejects outright. buildCloudflareWorkerUploadRequest() (src/lib/proxyRelay/cloudflareWorkerScript.ts) now builds the multipart body as a raw Buffer with an explicit boundary and Content-Type: multipart/form-data; boundary=… header, accepted verbatim by any fetch implementation. Regression guard: tests/unit/cloudflare-worker-upload-content-type-6416.test.ts + updated tests/unit/relay-deploy-5128.test.ts.

  • fix(security): SSRF-guard the provider-validation probes so they can no longer be used as an open relay to cloud-metadata endpoints — directHttpsRequest() (web-cookie / NVIDIA / Z.AI validation, all with a caller-controllable baseUrl) ran with guard:"none" + allowRedirect:true; it now applies getProviderValidationGuard() (default block-metadata: LAN/localhost allowed, 169.254.169.254/link-local IMDS rejected, opt-out via OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS) and allowRedirect:false so a provider can't 3xx-redirect the probe to metadata past the initial-URL guard. Regression guard: tests/unit/provider-validation-ssrf-guard.test.ts. (#6542) — see PR. (thanks @developerjillur)

  • fix(startup): AgentBridge's MITM proxy served a mismatched cert for 3 of the 4 antigravity/cloudcode-pa hosts it terminates TLS for, breaking interception (#6494) — src/mitm/server.cjs's TARGET_HOSTS decrypts all 4 hosts locally (daily-cloudcode-pa.googleapis.com, cloudcode-pa.googleapis.com, daily-cloudcode-pa.sandbox.googleapis.com, autopush-cloudcode-pa.sandbox.googleapis.com), but src/mitm/cert/generate.ts's self-signed cert only carried a SAN entry for the first host — a request to any of the other 3 got served a cert whose CN/SAN didn't match (confirmed via curl -k https://cloudcode-pa.googleapis.com/ showing CN=daily-cloudcode-pa.googleapis.com). generateCert() now sources its host list from ANTIGRAVITY_TARGET.hosts (src/mitm/targets/antigravity.ts, the single authoritative registry already kept in lock-step with server.cjs/dnsConfig.ts/mitmToolHosts.ts by their own drift tests) and emits a SAN entry for all 4 hosts instead of hard-coding a second, incomplete copy. Regression guard: tests/unit/agentbridge-antigravity-cert-hosts-6494.test.ts (asserts the host list covers all 4 hosts and that the real generated cert's SAN includes each one).

  • fix(resilience): a priority combo never fell back when a target masked credit/quota exhaustion behind an HTTP 200 (#6427) — validateResponseQuality() (open-sse/services/combo/validateQuality.ts) only inspected the response body's top-level error field when choices was ALSO missing/empty (the narrower #3424 empty-completion case); a masked 200 that echoed a non-empty stub choices alongside a structured error object, or a known exhaustion phrase (e.g. "insufficient credits", "quota exceeded") in the error envelope, slipped through as "valid" and the combo kept returning the dead target's response forever instead of failing over. The quality check now inspects the error envelope — a top-level OpenAI-shape error object, or a bounded, case-insensitive exhaustion-phrase match against error.message/error.code/error.type/top-level message/detail — unconditionally, before any shape-specific branch, and regardless of whether choices/output also look structurally present. The check never inspects choices[].message.content, so a legitimate completion that merely mentions "quota" or "credits" in assistant prose is not misclassified. Regression guard: tests/unit/masked-200-exhaustion-fallback-6427.test.ts.

  • fix(security): fail-closed CORS for the cookie/session-authed cloud-agent management routes — getCloudAgentCorsHeaders() reflected any caller's Origin and paired it with Allow-Credentials: true (a CSRF/exfil hole); it now defers to the central allowlist (resolveAllowedOrigin), echoes only an allowlisted origin with Vary: Origin, and emits Allow-Credentials only for an explicitly allowlisted origin — never for a CORS_ALLOW_ALL wildcard echo. Regression guard: tests/unit/cloud-agent-cors-failclosed.test.ts. (#6543) — see PR. (thanks @developerjillur)

  • fix(compression): adaptive-compression ladder ranked 6 real catalog engines (ccr, ionizer, relevance, llmlingua, llm, read-lifecycle) as if they didn't exist (#6533) — ladder.ts's AGGRESSIVENESS and REDUCTION_FACTOR maps only covered the 7 engines wired into DEFAULT_LADDER (session-dedup/rtk/headroom/lite/caveman/aggressive/ultra); every other engine registered in open-sse/services/compression/engines/index.ts — including ccr/llmlingua, which the ladder doc comment already says are intentionally addable via ladderOverride — fell through to aggressivenessOf()'s ?? 0 default (same rank as "off") and expectedReductionFactor()'s generic ?? 0.9 fallback, so floor-mode escalation could not rank or escalate past them once added to a custom ladder. Both maps now carry entries for all 6 missing real engines, rescaled ×10 (off:0ultra:70) and placed by each engine's documented stackPriority (ionizer between rtk/headroom, relevance before caveman, llmlingua/llm between aggressive and ultra, etc.); mcpAccessibility — named in the report — is not a registered CompressionEngine (it's a separate MCP tool-response truncation mechanism) and was correctly left out. Regression guard: tests/unit/ladder-engine-maps-6533.test.ts (asserts every id from listCompressionEngines() ranks above "off" with a non-default reduction factor). (thanks @chirag127)

  • fix(api): tool-call arguments could render as [object Object] sequences instead of the real JSON through the /anthropic (Anthropic-shape /messages) routing path (#6459) — appendToolCallArgumentDelta() (open-sse/utils/toolCallArguments.ts), the shared accumulator the streaming openai-to-claude response translator, openai-responses translator, and responsesTransformer all call to build up a tool call's arguments/input_json_delta buffer, treated any non-string incoming fragment as an empty string. Some upstreams deliver the full tool_calls[].function.arguments value as an already-parsed JSON object/array instead of the OpenAI-contracted JSON-encoded string; the old code silently discarded that fragment, leaving tool_use.input empty, and left downstream buffers open to a plain string coercion of the object ([object Object]) once client-side concatenation kicked in. appendToolCallArgumentDelta() now JSON.stringify()s a non-string, non-null object/array fragment into a valid JSON fragment instead of dropping it, so the assembled partial_json always parses back into the original structured value. Regression guard: tests/unit/anthropic-toolcall-args-6459.test.ts. (thanks @chirag127)

  • fix(providers): fusion combo returned the opaque "All fusion panel models failed" 503 even when only a minority of panel members were actually cooling down / rate-limited, and a user-supplied fusionTuning.minPanel=1 was silently overridden (#6454) — handleFusionChat() hard-clamped the quorum floor via Math.min(Math.max(2, cfg.minPanel), panel.length), so an operator-configured minPanel=1 never took effect: collectPanel()'s straggler-grace timer only starts once ok >= minPanel, and with the floor forced to 2 a single fast success plus N slow-failing stragglers never reached quorum, so the panel sat waiting instead of degrading to the survivor. Per-member failure reasons (straggler_dropped/timeout/threw/status_XXX/empty_content/unparseable) were also logged server-side but never surfaced in the 503 body, leaving operators unable to tell a rate-limit fan-fail from a broader outage. Fixed by honoring Math.max(1, cfg.minPanel) and threading a failures: Array<{ model, reason }> collector into the 503 message (model=reason per entry) — production fix already merged via #6521; this entry backfills the missing CHANGELOG bullet and adds an 11-member, fusion-free-scale regression test matching the original repro shape (a cooling minority must not sink a healthy majority; a genuinely all-failed panel still returns the documented 503). Regression guard: tests/unit/services/fusion-min-panel-and-failure-detail.test.ts + tests/unit/fusion-partial-panel-failure-6454.test.ts. (thanks @chirag127)

  • fix(providers): fusion combo strategy silently returned a panel member's raw answer instead of the configured config.judgeModel synthesis (#6455) — handleFusionChat()'s single-survivor "degrade gracefully" path (added for #6454) returned the lone panel answer directly whenever only one panelist succeeded, regardless of whether an explicit judgeModel was configured; with the default minPanel: 2 and a 2-model panel, any single flaky/rate-limited panelist forced this path on every request, so the configured judge (e.g. auto/claude-opus) was never invoked and the client-visible .model reflected whichever panelist happened to survive. The judge is now still invoked to synthesize a lone surviving answer whenever judgeModel is explicitly configured; the cheap direct-answer shortcut is kept only for the implicit case (no judgeModel set, where the "judge" is just panel[0]). Regression guard: tests/unit/fusion-judge-model-6455.test.ts + updated tests/unit/combo-fusion-strategy.test.ts. (thanks @chirag127)

  • feat(combo): sanitized diagnostic trace on an auto-combo terminal failure — instead of an opaque 503, a terminal combo failure now returns a whitelist-projected trace (candidate pool size, attempted count, excluded provider/reason codes, attempt order, and a terminal-reason code) via the new errorResponseWithComboDiagnostics()/sanitizeComboDiagnostics() in open-sse/utils/error.ts — provider/model ids and enumerated reason codes only, never keys/tokens/bodies, length- and count-capped. A reasoning-budget-exhausted panel now returns an actionable "increase max_tokens" message rather than a blind retry-limit 503. Regression guard: tests/unit/combo-diagnostics-trace.test.ts. (#6545) — see PR. (thanks @developerjillur)

  • fix(providers): image/diffusion models discovered from an upstream catalog (e.g. HuggingFace's live /v1/models) are no longer advertised as chat models (#6457) — the chat catalog builder defaulted synced models with no modality info to endpoints: ["chat"], so huggingface/stabilityai/stable-diffusion-xl-base-1.0 showed up in the chat /v1/models listing and returned 400 "not a chat model" when called. catalog.ts now skips any synced model already registered as an image model for that provider (via the new isRegisteredImageModel()), leaving getAllImageModels() to list it with the correct type: "image". Regression guard: tests/unit/image-model-not-in-chat-catalog-6457.test.ts.

  • fix(resilience): combo session stickiness never released a pin on a credits-exhausted/banned/expired account, permanently defeating failover for that conversation (#6692) — applySessionStickiness() (open-sse/services/combo/sessionStickiness.ts) gated the sticky pin only on 5h/weekly usage-percentage headroom, which is orthogonal to account availability: a credits_exhausted/banned/expired connection, or one still inside its rateLimitedUntil cooldown, reports perfectly healthy headroom, so the pin was force-promoted back to the front of the target list on every subsequent turn. clearStickyBinding() also had zero call sites in combo.ts's failure paths, so a quality-validation-rejected 200 (a masked daily-cap refusal) never released the pin either. The gate now also resolves the bound connection's terminal status/cooldown via a new injectable fetcher seam (fail-open on lookup errors, mirroring the existing saturation fetcher), and combo.ts's two dispatchers (handleComboChat/handleRoundRobinCombo) release the pin immediately at both their connection-exhaustion classification point and their quality-validation-failure branch via the new releaseStickyPinOnFailure(). Regression guard: tests/unit/repro-6692-sticky-terminal.test.ts + extended tests/unit/combo-session-stickiness.test.ts.

  • fix(test): replace the bare expect(true).toBe(true) tautology in playground-api-tab.test.tsx's SSE test and close the check:test-masking gap that let it slip through for a full cycle (#6404) — a prior pass (#6548) had already swapped the literal to expect(sendBtn).toBeDefined(), but that stayed just as vacuous: the test's fetch mock returned an empty /v1/models list, so ApiTab's Send button is always disabled (!selectedModel) and the SSE branch never runs — the "SSE infra is verified" comment was never true. The test now mocks a real model, drives the model <select> to enable Send, asserts sendBtn.disabled === false before clicking, and asserts the streamed SSE delta ("Hello!") actually reached the response editor. Root cause on the detector side: check-test-masking.mjs's tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (headExtTaut > baseExtTaut) and no-ops locally when GITHUB_BASE_SHA/GITHUB_BASE_REF are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (scanBareTautologies + countBareTautologies) over every git-tracked test file for the bare expect(true).toBe(true) / assert.equal(1,1) / assert.strictEqual(1,1) patterns specifically (deliberately excluding assert.ok(true), which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: tests/unit/check-test-masking.test.ts (new scanBareTautologies/countBareTautologies cases) + tests/unit/ui/playground-api-tab.test.tsx. (thanks @chirag127)

  • fix(oauth): Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh (#6352) — updateProviderCredentials() (the shared onPersist callback for the manual "Refresh token" route, the reactive per-request refresh in chat.ts, and the Codex/Claude auth-file importers) correctly reused the stored refresh_token, persisted the new access_token, and replaced a rotated refresh_token, but never cleared the stale testStatus/lastError*/errorCode fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets testStatus to "active" and clears lastError, lastErrorAt, lastErrorType, lastErrorSource, and errorCode (an explicit testStatus from the caller still wins). Regression guard: tests/unit/codex-oauth-refresh-persist-6352.test.ts.

  • perf(health): short-TTL (1s) cache for the frequently-polled GET /api/monitoring/health payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on DELETE (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: tests/integration/monitoring-health-cache.test.ts. (#6553) — see PR. (thanks @developerjillur)

  • fix(resilience): headroom combo routing did not always select the Codex account with the most free quota (#6379) — orderTargetsByHeadroom (open-sse/services/combo/quotaStrategies.ts) already loaded the per-connection DB snapshot (with decrypted credentials) via expandTargetsByQuotaAwareConnections, but discarded it before calling getSaturation; for Codex, fetchCodexSaturation forwards straight to fetchCodexQuota(connectionId, connection), which needs connection (or a prior registerCodexConnection() call, which never happens before headroom ranking runs) to read accessToken — so it returned null for every candidate, saturation failed open to 0 across the board, and ranking fell back to the original combo order regardless of actual free quota. getSaturation() and the headroom SaturationFetcher seam now accept and thread the loaded connection snapshot through to fetchCodexQuota. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: tests/unit/headroom-codex-quota-snapshot-6379.test.ts. (thanks @eidoog)

  • fix(providers): custom models a provider actually has are no longer dropped from the Free Provider Rankings when both "Configured only" and "Available only" filters are applied (#6368), follow-up to #6150freeProviderRankings.ts::getProviderModels() only ever walked the static open-sse/config/providerRegistry.ts catalog, so a user-added custom model (e.g. a Puter claude-fable-5 model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (db/models.ts::getCustomModels) into that candidate list via a new pure, de-duping mergeProviderModels() helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: tests/unit/free-provider-rankings-custom-models-6368.test.ts. (thanks @shabeer)

  • fix(playground): accept a valid dashboard session for GET/POST /api/playground/presets under REQUIRE_API_KEY=true — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; checkAuth() now accepts a management/dashboard session (requireManagementAuth) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: tests/integration/presets-dashboard-auth.test.ts. (#6554) — see PR. (thanks @developerjillur)

  • fix(providers): cloudflare-ai no longer silently drops image/non-text content parts (#6390) — transformRequest()'s flattenContent() (added for #2539 to satisfy the Workers AI /ai/v1/chat/completions plain-string content requirement) mapped any non-text OpenAI content part (e.g. image_url) to "" and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level chatCore.ts catch already routes through buildErrorBody()/sanitizeErrorMessage() (same pattern as buildUrl()'s missing-Account-ID error). Regression guard: tests/unit/cloudflare-ai-image-parts-6390.test.ts.

  • fix(providers): stop Antigravity connections from falsely reporting all-accounts quota-exhausted (#6295) — genericQuotaFetcher.ts::percentUsedForQuota() ignored the fractionReported flag and defaulted an unreported model's remainingPercentage to 0, which computed as 100% used; since convertUsageToQuotaInfo() takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into limitReached and quotaPreflight skipped it. percentUsedForQuota() now returns null (unknown, window ignored) whenever fractionReported === false, before falling back to remainingPercentage. Regression guard: tests/unit/generic-quota-fetcher.test.ts.

  • fix(fusion): the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter chatCore sharing the client's headers, so they derived the same Idempotency-Key/x-request-id and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on nexa/conversation-fusion). composeIdempotencyKey() now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: tests/unit/idempotency-fusion-collision.test.ts. (#6558) — see PR. (thanks @developerjillur)

  • fix(providers): grok-cli (Grok Build) now strips reasoning_effort/reasoning before forwarding the request (#6288) — Claude Code sends reasoning_effort on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; transformRequest()'s existing UNSUPPORTED sampling-param strip list (#5273) never covered it. Regression guard: tests/unit/grok-cli-reasoning-strip-6288.test.ts.

  • fix(cli): omniroute serve no longer hangs silently on a readiness timeout (#6321) — the child server's stdout was piped to "ignore" whenever --log/OMNIROUTE_SHOW_LOG wasn't set (the default), discarding any debug output, and runWithSupervisor's waitForServer(...).then((up) => { if (up) {...} }) had no else branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (ServerSupervisor.getRecentLog()), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: tests/unit/cli-serve-readiness-timeout-6321.test.ts.

  • fix(pricing): Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" (#6325) — pricingSync.ts kept sync state (lastSyncTime, lastSyncModelCount) in module-level vars, but the background periodic sync (instrumentation-node.ts) and the dashboard status route (/api/pricing/sync) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; getSyncStatus() read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new pricing_sync_status key_value namespace and getSyncStatus() falls back to it when the local module instance never ran a sync itself. Regression guard: tests/unit/pricing-sync-cross-instance.test.ts.

  • fix(api): stop spuriously 403-ing "Invalid request origin" on POST /api/providers/health-autopilot/actions for Docker/LAN dashboard requests (#6277) — the route carried a duplicate per-route validateBrowserMutationOrigin check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips PEER_IP_HEADER before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: tests/unit/serial/provider-health-autopilot.test.ts.

  • fix(resilience): a bare, unrecognized 403 from a no-credential (authType:"none") provider like mimocode or theoldllm no longer permanently bans the connection (#6315, #6345) — classifyProviderError()'s 403 branch only exempted apikey providers from the terminal FORBIDDEN classification, so these free/stateless proxies (no real account/credential to revoke) fell through to FORBIDDEN on the first unmatched 403 and got isActive:false, testStatus:"banned" with no cooldown or retry. The exemption now also covers authType:"none" providers, returning null (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: tests/unit/errorClassifier-noauth-403-6315.test.ts.

  • fix(providers): the Auggie (Augment CLI) executor no longer fails on Windows with spawn EINVAL (#6304) — the global-npm install exposes auggie as a .cmd shim, which Node's child_process.spawn cannot launch on win32 without shell: true. Both spawn sites (streaming + the auggie --version test) now go through a shared buildAuggieSpawnOptions() that sets shell: process.platform === "win32"; the argv (built by buildAuggieArgs() with a registry-validated model and a trailing -- end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: tests/unit/auggie-win32-spawn-6304.test.ts.

  • fix(api): the dashboard "Test model" action is now a clean connection test (#6240) — modelTestRunner sent its probe request without an explicit compression override, so whenever the operator's global compression.enabled flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send X-OmniRoute-Compression: off, and chatCore honors an explicit off header even when compression.enabled is globally true. Regression guards: tests/unit/model-test-runner-compression-off-6240.test.ts, tests/integration/test-model-compression-off-6240.test.ts.

  • fix(startup): an update/restart could crash the whole server at boot with TypeError: Cannot create property 'message' on string 'Database closed', masking the real failure and 500-ing every request until manually restarted (#6560, plausibly the root cause of #6594's post-upgrade 500) — driverFactory.ts::preInitSqlJs() cached its sql.js WASM adapter per file path in a globalThis-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by gracefulShutdown/resetDbInstance racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string "Database closed" (not an Error) straight out of instrumentation-node.ts's previously-unguarded ensureDbInitialized() call, and Next.js's internal registerInstrumentation() wrapper unconditionally does err.message = ... on whatever register() rejects with — assigning .message on a primitive string throws in strict mode, so the secondary TypeError is what actually crashed the process. Fixed in two parts: preInitSqlJs() now evicts a closed cached adapter and creates a fresh one instead of returning it; a new ensureDbReadyForBoot() wraps the DB-init call, normalizes any non-Error throw via normalizeBootError(), and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real Error. Regression guard: tests/unit/instrumentation-database-closed-6560.test.ts.

  • fix(api): POST /api/keys no longer hangs 20–90+ seconds on a fresh install, even with valid auth (#6570) — cloudEnabled defaults to true in src/lib/db/settings.ts::getSettings() on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional await syncKeysToCloudIfEnabled() always attempted a real outbound fetch() to CLOUD_URL via syncToCloud(); when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (POST /api/keys/:id/regenerate, GET /api/combos), which never touch this side effect at all. src/app/api/keys/route.ts now dispatches syncKeysToCloudIfEnabled() fire-and-forget (void) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: tests/unit/api-keys-create-no-hang-6570.test.ts (asserts the route resolves in well under 2s even when the Cloud-sync fetch() is stubbed to never settle) + updated tests/integration/api-keys.test.ts (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting).

  • fix(api): editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved (#6562) — createProviderConnection() (src/lib/db/providers.ts) auto-increments a new connection's priority to MAX(priority)+1 per provider with no upper bound, and OAuth-imported connections (Codex codex-auth/import / import-bulk, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through createProviderSchema's Zod validation at all, so nothing ever capped that value; EditConnectionModal's handleSubmit always resends the connection's current priority unchanged on every save, and updateProviderConnectionSchema capped priority/globalPriority at max(100) — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to max(100_000) on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: tests/unit/codex-connection-edit-6562.test.ts (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request").

  • mimocode: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz)

  • fix(cli): compression CLI REST fallback now reads/writes the canonical defaultMode field (surfaced as strategy) instead of a nonexistent engine key, and table output renders nested objects as JSON instead of [object Object] (#6571 — thanks @charleszolot)

  • fix(providers): web-cookie providers without a providerRegistry.ts entry (lmarena, gemini-business, poe-web, venice-web, v0-vercel-web) now report unsupported: true instead of silently "OK" (#6309) — validateWebCookieProvider() (src/lib/providers/validation.ts) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to WEB_COOKIE_PROVIDERS[provider].website was proposed, but live verification showed the ${website}/models probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is arena.ai, not lmarena.ai; Poe's real endpoint is a GraphQL POST, not a REST /models), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns unsupported (no network call) instead of a false positive. Regression guard: tests/unit/web-cookie-validation-fallback.test.ts. (thanks @oyi77)

  • fix(api): POST /api/middleware/hooks and PUT /api/middleware/hooks/[name] no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned error?.message directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through sanitizeErrorMessage() from open-sse/utils/error.ts. Regression guard: tests/unit/middleware-hooks-error-sanitization.test.ts.

  • fix(docker): compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct node-gyp rebuild inside node_modules/better-sqlite3, instead of npm rebuild better-sqlite3 (#6700) — the builder stage installs dependencies with npm ci --ignore-scripts (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; npm rebuild <pkg> re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking node-gyp rebuild directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts. (thanks @nowhats-br)

  • fix(providers): the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (export default { fetch(...) }) with main_module metadata; Cloudflare's Workers upload API parses a plain application/javascript script part as Service Worker syntax regardless of the main_module metadata field, and main_module requires the script to actually be an ES module (top-level export), so the mismatch still rejected the upload (#6496). buildCloudflareWorkerScript() (src/lib/proxyRelay/cloudflareWorkerScript.ts) now emits Service Worker syntax (addEventListener("fetch", ...), no top-level export) and the upload metadata uses body_part instead of main_module. Regression guard: tests/unit/relay-deploy-5128.test.ts (asserts the emitted script has no export default, registers a fetch listener, and the upload metadata carries body_part/omits main_module; also proves the inlined isPrivateHostname() SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like [::1]/[fd00::1] after the script-body rewrite). (thanks @SeaXen)

  • fix(providers): ChatGPT Web (chatgpt-web) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. citeturn0search0) and url… inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using message.metadata.content_references (#6635) — cleanChatGptText() now resolves content_references (grouped webpages, footnote sources, inline webpage/url mentions) into [label](url) Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro stream_handoff polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (open-sse/executors/chatgpt-web/citations.ts) to keep the executor under the frozen file-size cap. Regression guard: tests/unit/chatgpt-web-citations.test.ts (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape)

  • fix(dashboard): the live-dashboard WebSocket descriptor handshake (GET /api/v1/ws?handshake=1) and the lightweight GET /api/health/ping liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public (#6335) — clientApiPolicy required a bearer/dashboard-session before the WS route handler could even return its own wsAuth/protocol descriptor, and /api/health/ping was never added to PUBLIC_READONLY_API_ROUTE_PREFIXES despite its own docstring documenting it as "No auth required." clientApiPolicy.evaluate() now allows an anonymous {kind:"anonymous", id:"ws-handshake"} subject for GET/HEAD/OPTIONS on /api/v1/ws?handshake=1 (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and /api/health/ping is now in PUBLIC_READONLY_API_ROUTE_PREFIXES. Regression guard: tests/unit/authz/client-api-policy.test.ts (WS handshake allowed, including relative request URLs), tests/unit/public-api-routes.test.ts, and tests/unit/authz/classify.test.ts (/api/health/ping classified PUBLIC). (thanks @JxnLexn)

  • fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing jules cloud-agent pattern (#6142)

  • fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272)

  • fix(api): merge tool_call continuation deltas that carry only id (no index) so tool-call arguments are no longer split/lost in request/response logs (#6276)

  • fix(providers): modernize the lmarena provider for the Arena.ai rebrand — route chat through arena.ai create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the lmarena/lma wire id for back-compat (#6280) — thanks @backryun

  • fix(providers): web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + kimi-auth cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2).

  • fix(logs): the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev).

  • fix(providers): update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev).

  • fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343)

  • fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377)

  • fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799)

  • fix(docs): document Turbopack build memory tradeoff and OMNIROUTE_USE_TURBOPACK=0 webpack fallback for RAM-constrained machines (#6409)

  • fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491)

  • fix(providers): honor the max_token capability override in the reasoning-token-buffer output cap (#6524)

  • fix(dashboard): the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root images/ (not a served path); moved to public/images/ so Next.js serves them (#6538 — thanks @ianriizky).

  • fix(routing): the auto combo's no-auth candidate pool now honors a disabled provider connection's own isActive=false (the toggle on the main Providers grid card), not just the separate global blockedProviders setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation (#6557).

  • fix(sse): server-tool literal names (e.g. web_search) are preserved in message history and tool_choice instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev).

  • fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623)

  • fix(db): share one in-flight sql.js load across concurrent preInitSqlJs() callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628)

  • fix(db): unwrap lone named-parameter objects before sql.js stmt.bind() so @/:/$-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802)

  • fix(db): break probe-failed/restore loop on large storage.sqlite (#6632 — thanks @KooshaPari).

  • fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize in validate-release-green's failure-line detector (#6634)

  • fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637)

  • fix(cli): Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub).

  • fix(providers): removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun).

  • fix(sse): requests rejected before handleChatCore (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in usage_history too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698).

  • fix(sse): unwrap bare {function:{…}} tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis)

  • fix(oauth): stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin)

  • fix(codex): detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit)

  • fix(volcengine): clamp max_tokens to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820)

  • fix(antigravity): surface aborted/malformed Gemini tool calls (e.g. MALFORMED_FUNCTION_CALL) as an explicit non-end_turn finish reason instead of a silent clean completion. (thanks @anhdiepmmk)

  • fix(routing): the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and getExplicitModelOutputCap falls through to the registry/spec cap when a synced capability row exists without a numeric limit_output (#6714) — thanks @xz-dev

  • fix(api): omniroute health (and health components/health watch) returned Error: HTTP 404 (#6677) — bin/cli/commands/health.mjs called apiFetch("/api/health", ...), a route that was moved to GET /api/monitoring/health (src/app/api/monitoring/health/route.ts) without updating the CLI; src/app/api/health/ on disk only has degradation/route.ts and ping/route.ts, no top-level handler. runHealthCommand()/runHealthComponentsCommand() now call /api/monitoring/health and read its actual payload shape (activeConnections, circuitBreakers: {open, halfOpen, closed}, memoryUsage) instead of the old, nonexistent requests/breakers/cache/memory fields. Regression guard: tests/unit/cli-health-monitoring-route.test.ts.

  • fix(startup): webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in StudioConfigPane.tsx/ChatTab.tsx (#6584) — src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx (the component) and reasoningControls.ts (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form from "./reasoningControls", the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to reasoningControlUtils.ts (no collision) and updated the 3 import sites. Regression guard: tests/unit/case-collision-6584.test.ts (scans src//open-sse/ for any same-directory, case-only filename collision). (#6584)

  • fix(build): Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing src/lib/agentSkills/generator.ts (603 warnings reported on v3.8.46, up from 379 on v3.8.45) (#6582) — generator.ts's outputBase is built as path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir), where outputDir is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic readdirSync/rmSync/readFileSync/writeFileSync call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (skills/<id>/SKILL.md, ~48 known IDs), next.config.mjs's turbopack.ignoreIssue (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing webpack.ignoreWarnings/isNextIntlExtractorDynamicImportWarning precedent already in the same file for the webpack path. Regression guard: tests/unit/next-config.test.ts (asserts the turbopack.ignoreIssue rule shape targeting src/lib/agentSkills/**).

  • fix(providers): Codex Desktop requests to gpt-5.3-codex-spark failed with [400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark, even on paid-plan accounts (#6651) — CodexExecutor.transformRequest (open-sse/executors/codex.ts) only dropped the Codex Desktop-injected image_generation hosted tool when isCodexFreePlan() matched the account's plan, with no awareness that Spark-scope Codex models reject image_generation upstream regardless of plan. dropImageGeneration now also drops it when getCodexModelScope(model) === "spark" (the existing Spark classifier from open-sse/config/codexQuotaScopes.ts), independent of account plan. Regression guard: tests/unit/codex-spark-image-generation.test.ts (thanks @alltomatos for independently catching and fixing it via #6819).

  • fix(providers): the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order (#6687) — QuotaCardExpanded.tsx's sortQuotasByRemaining() (added in #5977) was applied unconditionally via useMemo(() => sortQuotasByRemaining(quotas), [quotas]), undoing the deterministic CODEX_QUOTA_ORDER/GLM_QUOTA_ORDER window order quotaParsing.ts's sortCodexOrder()/sortGlmOrder() (added in #6336) already established for Codex and the GLM family — since #6336 never touched QuotaCardExpanded.tsx, the two orderings never composed, so e.g. a Codex session window with less headroom than weekly rendered after it instead of staying first. A new hasFixedQuotaOrder() (quotaParsing.ts) and resolveQuotaDisplayOrder() (QuotaCardExpanded.tsx) now skip the remaining-% re-sort for providers with a fixed window order, threading providerId from QuotaCard.tsx through to the display layer; every other provider still gets the remaining-% sort. Regression guard: tests/unit/quota-card-expanded-fixed-order-6687.test.ts.

  • fix(i18n): pt-BR was missing 194 UI keys present in en.json — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 providers.* keys are disjoint, present-but-untranslated sentinels caused by a separate providerText() fallback bug) (#6695) — scripts/i18n/sync-ui-keys.mjs (which mirrors newly-added en.json keys into every locale) wasn't re-run after recent en.json additions, and the CI i18n:check-ui-coverage gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into src/i18n/messages/pt-BR.json (translated to Brazilian Portuguese, no leftover __MISSING__ markers) via npm run i18n:sync-ui -- --locale=pt-BR + manual translation. Regression guard: tests/unit/i18n-pt-br.test.ts (new case asserting full en.jsonpt-BR.json key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage).

  • fix(startup): omniroute --mcp crashed at Node ESM link time with ERR_MODULE_NOT_FOUND for ioredis on installs where the published MCP bundle didn't happen to have ioredis rescued from a parent node_modules (#6559) — src/shared/utils/rateLimiter.ts had a top-level static import Redis from "ioredis"; that module is only ever reached via a lazy await import(...) several call-sites deep in the MCP tool chain, but esbuild's --packages=external bundling of the MCP server (scripts/build/prepublish.ts Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled dist/open-sse/mcp-server/server.js, forcing Node to resolve ioredis at module-link time — before any --mcp startup code runs — and ioredis is not guaranteed to ship in the MCP-only bundle's node_modules. getRedisClient() now lazily imports ioredis on first use (matching the established soft-dependency pattern in src/lib/quota/redisQuotaStore.ts) while still throwing synchronously when Redis isn't configured. Regression guard: tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static ioredis import remains, while the pre-existing lazy await import("ioredis") in redisQuotaStore.ts stays intact).

  • fix(providers): Kiro sent the adaptive-thinking additionalModelRequestFields envelope for claude-sonnet-4.5/claude-haiku-4.5, which Kiro/CodeWhisperer rejects upstream with a raw [400]: additionalModelRequestFields is not supported for this model (#6576) — buildKiroPayload() (open-sse/translator/request/openai-to-kiro.ts) gated the field on the generic Anthropic-API supportsReasoning() capability flag, which is true for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only claude-sonnet-5 is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (supportsKiroAdaptiveThinking() in open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts) now gates the envelope instead. Regression guard: tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts.

  • fix(translator): Cursor's local Subagent tool call is no longer rejected with cloud_base_branch may only be specified when environment equals cloud — the Responses→Chat tool-arg cleanup (stripEmptyOptionalToolArgs) was scoped to Claude Code's Read tool only, so Cursor's Subagent tool passed through with the cloud-only cloud_base_branch: "" (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of Read + Subagent; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: tests/unit/openai-responses-subagent-strip-2446.test.ts. (thanks @like3213934360-lab)

  • fix(translator): GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's id and function.name in separate SSE chunks) no longer produce an empty tool name / No such tool available: error through the Claude /messages path — the openai-to-claude streaming translator emitted content_block_start immediately on the id-only chunk with an empty name, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now defers content_block_start until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted tool_use always carries the real name. Regression guard: tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts. (thanks @itiwant)

  • fix(resilience): OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window (#6638) — shouldPreserveQuotaSignalsFor429()/checkFallbackError() (open-sse/services/accountFallback.ts) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and parseRetryFromErrorText() also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (looksLikeQuotaExhausted()) now overrides the apikey-category default via the new shouldPreserveQuotaSignals() (open-sse/services/quotaResetParsing.ts), and parseDayGranularityResetMs() parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: tests/unit/issue-6638-ollama-quota.test.ts + 2 aligned tests/unit/account-fallback-service.test.ts cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text.

  • fix(resilience): a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted (#6696, relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (${rowId}|fp|${fingerprint}, src/lib/combos/builderOptions.ts), but expandTargetsByFingerprints() (open-sse/services/combo/fingerprintExpansion.ts) looked that composite string up directly in connectionById (keyed by real DB row ids), got undefined, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. expandTargetsByFingerprints() now splits the |fp| composite id back into the real connection row id + the pinned fingerprint (new splitFingerprintPin() helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new pinnedFingerprint field) instead of the inert composite string. Regression guard: tests/unit/combo-fingerprint-pin-6696.test.ts.

  • fix(api): Responses passthrough emitted event-only SSE frames (no data: line) for every dropped commentary event, breaking the OpenAI Python SDK's sse.json() parser (#6561), follow-up to #6199/#6232 — the commentary-drop continue; branches in open-sse/utils/stream.ts skipped the data: line for a dropped commentary event but never cleared the already-buffered event: line for that same frame, so the next blank line flushed the stale event: line alone. Both drop sites now call clearPendingPassthroughEvent() before continue, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new open-sse/utils/responsesCommentaryDrop.ts so the fix does not grow the frozen stream.ts. Regression guard: tests/unit/responses-commentary-event-frame-6561.test.ts (realistic event:\ndata:\n\n frames — the existing #6199 test only used bare data: lines and never exercised this path).

  • fix(compression): /api/compression/preview's top-level originalTokens/compressedTokens diverged from engineBreakdown[0]'s counts for the same single-engine run (tiktoken outer counts vs the JSON.stringify(...).length/4 estimate per engine), worst on small inputs. A new reconcileSingleEngineTokens() overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched (#6488). Regression guard: tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts.

  • fix(resilience): account selection could pick an account already out of quota upstream on every credentialed route except chat/codex (#6686) — getProviderCredentials() (src/sse/services/auth.ts) only skips a connection when a local cache already flags it exhausted (isQuotaExhaustedForRequest/src/domain/quotaCache.ts); it never itself calls the registered upstream QuotaFetcher. Only getProviderCredentialsWithQuotaPreflight() performs that live upstream check, and it was wired into exactly 2 call sites (src/sse/handlers/chat.ts, src/app/api/internal/codex-responses-ws/route.ts) — every other credentialed route (rerank, images/generations, images/edits, audio/transcriptions|speech|translations, videos/generations, music/generations, ocr, providers/[provider]/embeddings, providers/[provider]/images/generations, web/fetch, moderations, search) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through getProviderCredentialsWithQuotaPreflight() instead, matching chat/codex coverage. Regression guard: tests/unit/issue-6686-quota-preflight-coverage.test.ts (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account).

  • fix(api): reasoning_content (extended-thinking text) was silently dropped from /v1/chat/completions SSE on the claude-web and v0-vercel-web executors (#6662) — every chunk builder in both adapters hardcoded delta: { content: ... } with no reasoning path, unlike the established pattern already used by default.ts/deepseek-web.ts/bedrock.ts and the real-Anthropic-API claude-to-openai.ts translator (thinking_deltareasoning_content). v0-vercel-web.ts now forwards an upstream delta.reasoning_content field (streaming and non-streaming) the same way deepseek-web.ts does. claude-web.ts's buildClaudeStreamingResponse now maps a content_block_start(type: "thinking")/content_block_delta(delta.thinking) pair onto delta.reasoning_content, and claude-web/payload.ts's transformToClaude() no longer hardcodes thinking_mode: "off" — a new wantsExtendedThinking() derives it from the request's reasoning_effort/reasoning.effort/thinking.type signal, so extended thinking can actually be requested. Regression guard: tests/unit/issue-6662-repro.test.ts (RED→GREEN for both adapters).

  • fix(api): the compression config PUT schema now accepts enableRenderers for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756).

  • fix(api): raised the provider apiKey length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos).

  • fix(ci): publish electron-updater latest*.yml manifests in electron release assets so auto-update can find them (#6766)

  • fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768) (#6769 — thanks @chirag127).

  • fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772)

  • fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773)

  • fix(codex): bump the default Codex CLI client identity from 0.142.0 to 0.144.0 for compatibility with newer Codex-backed models (#6780) — thanks @quanturbo

  • fix(ci): the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under --import tsx/esm, but tests/unit/dashboard/** requires the --import tsx CJS transform (ESM-only deep imports like @lobehub/icons/es/*), exactly as the canonical test:unit:ci:shard already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787).

  • fix(translator): read PDF/video file_data attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to document/inlineData and videos keep their video/mp4 mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753).

  • fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED (#6791 — thanks @Pitchfork-and-Torch).

  • fix(api): the compression config PUT schema (stackedPipelineStepSchema) now accepts every ENGINE_CATALOG id — the structural engines session-dedup/ccr/headroom/relevance/llmlingua/omniglyph and the aggressive ultra intensity — so a GETPUT round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch).

  • fix(cursor): send the Agent CLI build id as x-cursor-client-version so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell).

  • fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800)

  • fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803)

  • fix(codex): strip include from compact responses requests (#6805 — thanks @yinaoxiong).

  • fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806)

  • Request count by provider & date: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New getProviderDailyUsageRows() query (src/lib/db/usageAnalytics.ts) and its own GET /api/usage/requests-by-provider-date route (kept separate from the frozen /api/usage/analytics route). Regression guard: tests/unit/db-provider-daily-usage-4009.test.ts. (#4009 — thanks @tjengbudi)

  • fix(resilience): an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off (#3709) — the upstream 429 body ("you (<account>) have reached your weekly usage limit") was invisible to checkFallbackError's existing subscription-quota-text classifier (Issue #2321) because that branch is gated by shouldUseQuotaSignal, which is oauth-only, so apikey providers like ollama-cloud fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new isWeeklyUsageLimitText/buildWeeklyQuotaFallback classifier (extracted, with the existing subscription-quota logic, into a new open-sse/services/quotaTextCooldowns.ts module so the frozen accountFallback.ts didn't have to grow) runs unconditionally and applies a 24h QUOTA_EXHAUSTED cooldown regardless of provider category. Regression guard: tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts.

  • fix(providers): an explicit thinking.budget_tokens: 0 is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos).

  • fix(bootstrap): filter empty process.env values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu).

  • fix(providers): classify upstream 404 responses as MODEL_NOT_FOUND (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu).

  • fix(mcp): de-duplicate TOTAL_MCP_TOOL_COUNT by tool name instead of double-counting collections (#6854)

  • fix(usage): xAI's exact provider-reported cost_in_usd_ticks no longer silently accepts null/""/negative values as a valid $0 exact cost — extractUsageFromResponse() (open-sse/handlers/usageExtractor.ts) and normalizeUsage() (open-sse/utils/usageTracking.ts) now require typeof value === "number" && Number.isFinite(value) && value >= 0 instead of coercing with Number(x), so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus $0. Regression guard: tests/unit/xai-exact-cost-2453.test.ts (rejects null/empty/negative exact costs on both call sites). (#6856 — thanks @KooshaPari)

  • fix(plugin): the @omniroute/opencode-plugin dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible opencode--prefixed provider id into model routing fields (ModelV2.id/providerID, 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" (#6859).

  • fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876)

  • fix(sse): defer response.completed until a trailing usage-only chunk arrives on /v1/responses streams (#6906)

  • fix(cli): ship head-response-guard.cjs into the standalone bundle — server-ws.mjs imported it without a matching EXTRA_MODULE_ENTRIES entry, so every build:release dist crashed at boot with ERR_MODULE_NOT_FOUND; a new regression test derives the required sidecars from server-ws.mjs imports (#6908)

  • fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911)

  • fix(sse): rename client-sent max_completion_tokens to max_tokens for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912)

  • fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914)

  • fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925)

  • fix(sse): Qwen Web executor no longer sends [object Object] when a message uses structured (array) content — the text parts are now flattened (#6927)

  • perf(api): relay chat-completions routes now thread the already-fetched RelayToken into checkRateLimit, skipping a redundant SELECT * FROM relay_tokens WHERE id = ? re-query on every request (#6930)

  • fix(sse): normalizeCodexMessageContentPart now rewrites explicit type: "input_text" (not just type: "text") to output_text on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as input_text is no longer rejected by the Codex/OpenAI backend (#6932)

  • fix(sse): omit removed attachments field from Muse Spark Web (Meta AI) persisted GraphQL query to fix Unknown type "AttachmentInput" 502s (#6935)

  • fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936)

  • fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939)

  • fix(providers): openai->gemini transform now maps reasoning_effort: "none" to thinkingConfig.thinkingBudget: 0 (with includeThoughts: false), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu)

  • fix(sse): escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing \ from corrupting the generated Markdown link (#6944) — thanks @brick30llc-ctrl

  • fix(oauth): tokenHealthCheck now lowercase-normalizes conn.provider before checking ROTATING_REFRESH_PROVIDERS.has() and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947)

  • fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema default, generalize empty-optional stripping to any tool (not just Read/Subagent) via schema.required, and thread each tool's schema from the request's tools[] into the streaming response translator (#6951)

  • fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952)

  • fix(combos): embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only isChatCapable gate in addModelOption() has been removed (#6975).

  • fix(combos): when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's codestral-latest/codestral-2508 aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957).

📝 Maintenance

  • chore(release-captain): v3.8.47 pre-flight closed every deterministic release-tip base-red (#6967): restored the no-explicit-any severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (test.after() was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported relayProbeStats (db-rules), documented OMNI_MAX_CONCURRENT_CONNECTIONS, and allowlisted migration gap 121. (thanks @diegosouzapw)

  • chore(security): unbiased crypto digits for the doubao synthetic device id (CodeQL js/biased-cryptographic-random); 405 method-first for /api/keys/{id}/devices (dast-smoke); Zod-validation for POST /api/github-skills; the missing omni-github-skills registry entry + catalog count alignment. (thanks @diegosouzapw)

  • chore(quality): cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made validate-release-green suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw)

  • chore(open-sse): removed the vestigial // @ts-nocheck directive from open-sse/utils/usageTracking.ts (#6173) — tsc under the standard typecheck:core gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari)

  • chore(cli): the shell-completion cache paths (readCache/refreshCache/writeCache) in bin/cli/commands/completion.mjs no longer swallow errors into a bare catch {} (#6257) — each now binds the error and, when the new OMNIROUTE_DEBUG_COMPLETION env var is set, emits a [omniroute completion] diagnostic to stderr; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari)

  • chore(quality): validate-release-green --full-ci reproduces the full ci.yml static gate set locally — the pre-flight now reads ci.yml itself and runs every npm run check:* from the lint / quality-gate / quality-extended / docs-sync-strict / pr-test-policy jobs (-- ratchet flags preserved, test-masking against GITHUB_BASE_REF=main), skipping only the non-local pr-evidence/codeql-ratchet. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into nightly-release-green so a static base-red opens a tracking issue the night it lands. Regression guard: tests/unit/validate-release-green.test.ts (+5 extractCiGates cases).

  • refactor(usage): saveRequestUsage(entry: any) is now typed with a new UsageEntry interface mirroring the usage_history columns 1:1 (#3512) — the other stray anys in src/lib/usage/usageHistory.ts (getUsageHistory filter, the getUsageDb next-cursor cast, appendRequestLog's legacy tokens param, getRecentLogs's catch) were cleaned in the same pass, so the file now sits in the check:any-budget:t11 zero-any allowlist. The DB-entity ↔ TS-interface convention is documented in docs/architecture/CODEBASE_DOCUMENTATION.md §11.

  • Merge-train script (scripts/release/merge-train.sh): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the --admin evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: tests/unit/merge-train-plan.test.ts.

  • release: list-uncovered-commits.mjs now unions the CHANGELOG scan window with changelog.d/ fragment refs (filename <PR>- prefix + every #N in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap (#6857 via #6878)

  • chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift)

  • chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract

  • chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up)

  • ci: unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind USE_VPS_RUNNER (#6691); VPS_ALWAYS_ON keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693).

  • docs: routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and p2c casing fixed to match ROUTING_STRATEGY_VALUES (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663).

  • chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169

  • docs(readme): fix stale counts — 18 routing strategies (adds the missing pipeline row), 94 MCP tools, 12-factor Auto-Combo scoring.

  • chore(ci): fix two shared base-reds on the release tip that blocked the PR queue — register cliproxyapi-model-mapping-dispatch.test.ts in stryker.conf.json tap.testFiles (mutation-coverage gap left by #6903) and update provider-models-route-codex.test.ts to expect Codex client version 0.144.0 (stale assertion left by #6780's production bump).

  • docs: refresh llm.txt to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to docs/architecture/DESIGN_SYSTEM.md rewritten as a reference doc.

  • chore(security): scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the tests/boundary/*.live.test.ts files landed via #6786 — they now read OMNIROUTE_TEST_BASE / OMNIROUTE_TEST_BEARER / OMNIROUTE_TEST_COOKIE from the environment and stay gated behind RUN_BOUNDARY_LIVE=1.

🙌 Contributors

Thanks to everyone whose work landed in v3.8.47:

Contributor PRs / Issues
@AgentKiller45 #6863, #6866
@alltomatos #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821
@andrewmunsell #6774, #6779, #6795
@AndrianBalanescu #6828, #6829
@anhdiepmmk direct commit / report
@artickc #6363, #6763
@backryun #6280, #6675, #6862
@blackwell-systems #6838
@brick30llc-ctrl #6944
@charleszolot #6571
@chirag127 #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883
@chy1211 direct commit / report
@crochabe-cyber #4013
@deadcoder0904 #6665
@developerjillur direct commit / report
@eidoog direct commit / report
@enjoyer-hub #6647
@gdevenyi direct commit / report
@growab #6867
@hajilok #6126, #6833
@hamsa0x7 #6317, #6318, #6338
@herjarsa direct commit / report
@iamraydoan #6798
@ianriizky #6072, #6538
@itiwant direct commit / report
@janeza2 #6308
@JxnLexn #6776
@KooshaPari #6611, #6632, #6856
@like3213934360-lab direct commit / report
@lucasjustinudin direct commit / report
@lunkerchen #6320
@MikeTuev #6586, #6830
@Moseyuh333 #6294, #6728
@nowhats-br direct commit / report
@oyi77 #6907, #6926, #6929, #6946
@Pitchfork-and-Torch #6747, #6791, #6792
@pizzav-xyz #6648
@quanturbo #6780
@rafaumeu #6813
@rafpigna #6574
@rucciva #4125
@rushsinging #6807
@ryanngit direct commit / report
@samir-abis direct commit / report
@SeaXen direct commit / report
@shabeer direct commit / report
@Squawk7777 #6565
@strangersp #6587
@Thinkscape direct commit / report
@ThongAccount #6625, #6649
@tjengbudi #4009
@whale9820 direct commit / report
@Witroch4 #6753, #6762, #6790
@xz-dev #6323, #6330, #6702, #6714, #6727
@yinaoxiong #6805
@diegosouzapw maintainer