- 9router Codex import: the Codex bulk-import endpoint (
POST /api/oauth/codex/import) now accepts 9router's camelCase account export (accessToken/refreshToken/idToken/expiresAt+ nestedproviderSpecificData), not just snake_case —normalizeCodexImportRecordmaps 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-suppliedproviderSpecificDatawithout 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/responsesendpoint. (#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-cireproduces 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 existingcommon.search/common.noResultsi18n keys (zero new locale edits) and the sharedInputicon="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 intofilterSidebarSectionsByQuery()(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 persistedconfig.budgetFallback: "strict") makes an over-budget request fail fast withHTTP 402instead 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 existingX-OmniRoute-Budget/X-OmniRoute-Modeper-request controls (#6023/#6024/#6025), consolidated intoresolveRequestAutoControls(). 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
#3587reasoning-modelmax_tokensbuffer, 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 viaresolveComboStickyRoundRobinLimit()— per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A newproviderStrategiessetting (Zod-validated map,src/shared/validation/settingsSchemas.ts) lets a specific provider override the globalfallbackStrategy/stickyRoundRobinLimitwithout touching the account-wide default, wired intogetProviderCredentials()(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/iconsReact components →thesvg.orgCDN (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 thecli-skill-collectoragent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally viagetCliRuntimeStatus(), match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes andGET/POST /api/github-skillsnow 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). Theomniroute_github_skills_installMCP tool now reports the honestaction: "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 theOAUTH_PROVIDERScatalog alias) so<alias>/<modelId>routing resolves correctly, the OAuth refresh dispatch now routesclinepassto 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, sotryAwsSsoCache()now detects them (authMethod/provider === "externalidp") and refreshes via the org IdP's owntokenEndpoint(public-client OAuth2 refresh grant, no client secret), persistingTokenType: EXTERNAL_IDPgating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts;tokenEndpointis SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) -
Kiro long-lived API key auth: new
/api/oauth/kiro/api-keyroute +KiroService.validateApiKeylet 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 newchaosModeEnabledpermission (opt-in — disabled by default globally and per key).POST /api/chaos/run(dashboard session) andPOST /api/skills/collect/chaos(external Bearer-token) delegate to a sharedexecuteChaosRun()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), configurablemax_tokens(256–128k), a clear error whenstreamis 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 lettato detect the local install, so they're loopback-gated (LOCAL_ONLY_API_PREFIXES, Hard Rules #15/#17) in addition to the sharedrequireCliToolsAuth()management-auth guard every cli-tools route requires, and route errors throughsanitizeErrorMessage();src/lib/db/omp.tsisolates theompCLI'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_overridestable (source: "manual") — already the priority-0 sourcegetModelContextLimit()(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-modelsnow accepts an optionalcontextWindowOverride(number to set,nullto clear),GETsurfaces 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 erroringInvalid rerank model(#6574 — thanks @rafpigna) -
fix(api):
HEADrequests 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 aResponsebody forHEAD, but its page-rendering pipeline (next/dist/server/pipe-readable.js→pipeToNodeResponse, used for every app-router page/layout render — including thenot-foundboundary 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)HEADresponse had actually finished. A newscripts/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 existinghttp-method-guard.cjs/peer-stamp.mjswrappers, discards any body bytes written for aHEADrequest and forcesConnection: closeonce.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) —QuotaCardGridpreviously stacked every provider group in a single verticalflex flex-col, and each group's own card grid didn't go multi-column untilmd(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 withAPI_PORTin split-port setups, the deprecatedOMNIROUTE_DISABLE_LIVE_WSenv was consolidated intoOMNIROUTE_ENABLE_LIVE_WS(default enabled), and the WS path is now derived fromNEXT_PUBLIC_LIVE_WS_PUBLIC_URL's pathname (/live-wsfallback) (#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 withskip:<reason>techniques, preview (stable: false, off by default) (#6556). Dependency bumped toomniglyph@^1.0.2for 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 hardcodingdocker run, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). -
fix(sse): skip
thinkingConfigfor 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/responsesendpoint instead of the chat-completions bridge. (thanks @ryanngit) -
feat(models): add a Settings → AI "Model Overrides" UI plus
/api/model-capability-overridesCRUD and amodel_capability_overridestable, 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
rotationConfiglayer lets operators tune how connections rotate on failure, wired intoaccountFallback(#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/modelsusing 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 — newsrc/app/api/providers/[id]/models/discovery/codex.ts(normalization, version-gating, merge/enrich against the local catalog) covered bytests/unit/provider-models-discovery-split.test.tsandtests/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 editingCHANGELOG.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, andcheck:changelog-integritynow 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 sharedModelSelectModal(combo builder + CLI-code cards) already had search, but Playground'sStudioConfigPanemodel 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 viamatchesSearch), 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 existingcommon.searchi18n 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
retrieveUserQuotaresponse the fetcher already calls; it only appears in a separate, undocumentedretrieveUserQuotaSummaryRPC that groups models into families ("Gemini Models", "Claude and GPT models") with one weekly bucket per family. A newusage/antigravityWeeklyQuota.tsleaf 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 intogemini_weekly/claude_gpt_weeklyquota entries, merged into the samequotasmap the widget already renders generically. Regression guard:tests/unit/antigravity-weekly-quota-4017.test.ts(bucket parsing, the alternatequotaSummary-nested envelope, and end-to-end merge viagetUsageForProvider). -
feat(codex): Codex CLI compatibility shim — the Responses API
response.created/response.in_progress/response.completedpayloads now carry amodelfield (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 newisCodexOriginatedHeaders()(open-sse/config/codexIdentity.ts, reusing PR #3481'soriginator/User-Agent detection) makes chatCore's existing opt-inechoRequestedModelName(#1311) model-echo pipeline fire automatically for Codex clients regardless of the setting, detected by request headers so it still applies whencodex/gpt-5.5-xhighis routed through a combo to a non-codex upstream;echoModelInObject/echoModelInSseLine(open-sse/services/responseModelEcho.ts) now also rewrite the nestedresponse.modelfield the Responses API uses./v1/modelsstill returnsmodels: []for Codex (unchanged). Regression guard:tests/unit/codex-effort-model-echo-3697.test.ts. -
Z.ai Web (free web-session provider): new
zai-webweb-cookie provider drives the free chat.z.ai consumer chat UI via a pasted browser session cookie, distinct from the existing API-keyzai/glm/glm-cn/glmtproviders (api.z.ai) — modeled on thedoubao-web/venice-webcookie executors and the pre-existingchatglm-webcredential requirement/token-extraction entries.ZaiWebExecutor(open-sse/executors/zai-web.ts) posts tochat.z.ai/api/chat/completionswith the cookie forwarded both asCookieand asAuthorization: Bearer <token>, and normalizes both z.ai's internaldelta_content/phaseSSE envelope and a pass-through OpenAI-shapedchoices[].deltaframe into standard chat-completion chunks. Registered inWEB_COOKIE_PROVIDERS,WEB_SESSION_CREDENTIAL_REQUIREMENTS, the provider registry (zai-webentry, GLM-4.6/4.5/4.5V models), andtokenExtractionConfig.tsfor 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__/constructorpath segment never mutatesObject.prototype, and keys shadowing built-ins liketoStringnow 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:coredoes not cover dashboard TSX — follow-up #7021). (thanks @diegosouzapw) -
fix(combo):
comboStickyRoundRobinLimitnow defaults to inherit (null) instead of1— 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=0and 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
messagesbefore model resolution. (#6402, #6515 — thanks @chirag127) -
fix(providers): enrich the model_cooldown 429 body with a
retry_afterISO 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_startuntil GLM streams the tool name. (#6730 — thanks @diegosouzapw) -
fix(translator): strip empty
cloud_base_branchfrom 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_tokensto the Ark endpoint cap. (#6712 — thanks @diegosouzapw) -
fix(codex): surface capacity errors embedded in 200-OK SSE streams. (#6710 — thanks @diegosouzapw)
-
fix(sse): skip
thinkingConfigfor 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
getApiKeyMetadatacall in the embeddings route. (#6929 — thanks @oyi77) -
fix(db):
authTypefilter support ingetProviderConnections. (#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-unusedfilterVisible/filterHidden/filterByVisibility) rendered as the literal__MISSING__:<english>sentinel in 15 locales, including pt-BR (#6694) —providerText()(providerPageHelpers.ts) checkst.has(key)before falling back to clean English, andt.has()returnstrueeven when the stored value is the__MISSING__:sentinelscripts/i18n/sync-ui-keys.mjswrites 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) determinedinstalledpurely from binary resolution (known install paths + awhere/whichPATH 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.jsonon disk proves the tool was installed and used before. Upstream 9router's equivalent route already has this exact fallback. A newwithSettingsFallback()(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,installednow reportstrue. 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 onlydaily-cloudcode-pa.googleapis.comwhile the DB recordeddns_enabled=truefor the selected agent. Both functions now accept an optionalagentIdand resolve hosts viaALL_TARGETS;POST /api/tools/agent-bridge/agents/[id]/dnspasses the route'sidthrough 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 wrapsgenerateCert()(log + rethrow), theprovisionDnsEntries()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/removeDNSEntriesalso 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) swapsbody.modelto the best available vision-capable model, but that swap happens in the guardrail pipeline AFTERchat.tsalready calledenforceApiKeyPolicy()against the ORIGINAL model, so a key scoped to a narrowallowedModelslist could still execute against an unvetted (and possibly costlier) vision model the reroute picked.chat.tsnow 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 explicitsettings.visionBridgeModeloperator override (previously ignored, unlike the combo/describe path a few lines below it, which already respects it viagetVisionBridgeConfig). Regression guard:tests/unit/guardrails/visionBridge.test.ts(22 tests). (thanks @herjarsa) -
fix(auth): an API key restricted via
allowedModels/allowedComboscould bypass that restriction entirely over the Codex Responses-over-WebSocket bridge (#6564) —prepare()insrc/app/api/internal/codex-responses-ws/route.tsauthenticated the WS bridge's API key (authenticate()/authorizeWebSocketHandshake()) and honoredallowedConnections, but never calledenforceApiKeyPolicy(), the same model/combo policy gate the HTTP/v1/responsespath enforces viahandleChat()— so a key scoped to e.g.combo/model-1.0could still reach a direct Codex model likegpt-5.5through 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 normalAuthorizationheader, so a newenforceCodexWsApiKeyPolicy()builds an equivalentRequestcarrying an explicitAuthorization: Bearer <apiKey>header and callsenforceApiKeyPolicy()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 vianew vm.Scripton the request hot path (src/lib/middleware/registry.ts), the same RCE class as the already-gated/api/plugins/*;/api/middleware/is now inLOCAL_ONLY_API_PREFIXESso 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 requiredon a normal install (#6403) —POST /api/tools/agent-bridge/serverresolved the spawned MITM child's router key from only an explicitapiKeybody field (never sent by the AgentBridge UI — the schema has no such field) and theROUTER_API_KEYenv var (unset by default), sostartMitm()always received""and the child hard-exited, even though OmniRoute already had a usable API key in its own DB. A newresolveRouterApiKey()now falls back topickApiKeyForInternalUse()(the same DB-backed selector the combo-health-check / cloud-sync internal probes use), resolving in order: explicit key →ROUTER_API_KEYenv → 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 nativeFormDataand letfetchderive the multipart Content-Type automatically, but in productionglobalThis.fetchis patched withnode_modules/undici's own fetch (open-sse/utils/proxyFetch.ts), whoseFormData/Requestclasses differ from the runtime's globalFormData(same cross-realm class mismatch already fixed once for image edits in #3273); passing a nativeFormDatainstance through undici's patched fetch made it serialize the body as the literal string"[object FormData]"withContent-Type: text/plain;charset=UTF-8, which Cloudflare rejects outright.buildCloudflareWorkerUploadRequest()(src/lib/proxyRelay/cloudflareWorkerScript.ts) now builds the multipart body as a rawBufferwith an explicit boundary andContent-Type: multipart/form-data; boundary=…header, accepted verbatim by any fetch implementation. Regression guard:tests/unit/cloudflare-worker-upload-content-type-6416.test.ts+ updatedtests/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-controllablebaseUrl) ran withguard:"none"+allowRedirect:true; it now appliesgetProviderValidationGuard()(defaultblock-metadata: LAN/localhost allowed,169.254.169.254/link-local IMDS rejected, opt-out viaOMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS) andallowRedirect:falseso 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'sTARGET_HOSTSdecrypts 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), butsrc/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 viacurl -k https://cloudcode-pa.googleapis.com/showingCN=daily-cloudcode-pa.googleapis.com).generateCert()now sources its host list fromANTIGRAVITY_TARGET.hosts(src/mitm/targets/antigravity.ts, the single authoritative registry already kept in lock-step withserver.cjs/dnsConfig.ts/mitmToolHosts.tsby 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
prioritycombo 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-levelerrorfield whenchoiceswas ALSO missing/empty (the narrower #3424 empty-completion case); a masked 200 that echoed a non-empty stubchoicesalongside 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-shapeerrorobject, or a bounded, case-insensitive exhaustion-phrase match againsterror.message/error.code/error.type/top-levelmessage/detail— unconditionally, before any shape-specific branch, and regardless of whetherchoices/outputalso look structurally present. The check never inspectschoices[].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'sOriginand paired it withAllow-Credentials: true(a CSRF/exfil hole); it now defers to the central allowlist (resolveAllowedOrigin), echoes only an allowlisted origin withVary: Origin, and emitsAllow-Credentialsonly for an explicitly allowlisted origin — never for aCORS_ALLOW_ALLwildcard 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'sAGGRESSIVENESSandREDUCTION_FACTORmaps only covered the 7 engines wired intoDEFAULT_LADDER(session-dedup/rtk/headroom/lite/caveman/aggressive/ultra); every other engine registered inopen-sse/services/compression/engines/index.ts— includingccr/llmlingua, which the ladder doc comment already says are intentionally addable vialadderOverride— fell through toaggressivenessOf()'s?? 0default (same rank as"off") andexpectedReductionFactor()'s generic?? 0.9fallback, sofloor-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:0…ultra:70) and placed by each engine's documentedstackPriority(ionizerbetweenrtk/headroom,relevancebeforecaveman,llmlingua/llmbetweenaggressiveandultra, etc.);mcpAccessibility— named in the report — is not a registeredCompressionEngine(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 fromlistCompressionEngines()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 streamingopenai-to-clauderesponse translator,openai-responsestranslator, andresponsesTransformerall call to build up a tool call'sarguments/input_json_deltabuffer, treated any non-stringincomingfragment as an empty string. Some upstreams deliver the fulltool_calls[].function.argumentsvalue as an already-parsed JSON object/array instead of the OpenAI-contracted JSON-encoded string; the old code silently discarded that fragment, leavingtool_use.inputempty, and left downstream buffers open to a plain string coercion of the object ([object Object]) once client-side concatenation kicked in.appendToolCallArgumentDelta()nowJSON.stringify()s a non-string, non-null object/array fragment into a valid JSON fragment instead of dropping it, so the assembledpartial_jsonalways parses back into the original structured value. Regression guard:tests/unit/anthropic-toolcall-args-6459.test.ts. (thanks @chirag127) -
fix(providers):
fusioncombo 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-suppliedfusionTuning.minPanel=1was silently overridden (#6454) —handleFusionChat()hard-clamped the quorum floor viaMath.min(Math.max(2, cfg.minPanel), panel.length), so an operator-configuredminPanel=1never took effect:collectPanel()'s straggler-grace timer only starts onceok >= 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 honoringMath.max(1, cfg.minPanel)and threading afailures: Array<{ model, reason }>collector into the 503 message (model=reasonper 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):
fusioncombo strategy silently returned a panel member's raw answer instead of the configuredconfig.judgeModelsynthesis (#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 explicitjudgeModelwas configured; with the defaultminPanel: 2and 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.modelreflected whichever panelist happened to survive. The judge is now still invoked to synthesize a lone surviving answer wheneverjudgeModelis explicitly configured; the cheap direct-answer shortcut is kept only for the implicit case (nojudgeModelset, where the "judge" is justpanel[0]). Regression guard:tests/unit/fusion-judge-model-6455.test.ts+ updatedtests/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()inopen-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 toendpoints: ["chat"], sohuggingface/stabilityai/stable-diffusion-xl-base-1.0showed up in the chat/v1/modelslisting and returned400 "not a chat model"when called.catalog.tsnow skips any synced model already registered as an image model for that provider (via the newisRegisteredImageModel()), leavinggetAllImageModels()to list it with the correcttype: "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: acredits_exhausted/banned/expiredconnection, or one still inside itsrateLimitedUntilcooldown, 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 incombo.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), andcombo.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 newreleaseStickyPinOnFailure(). Regression guard:tests/unit/repro-6692-sticky-terminal.test.ts+ extendedtests/unit/combo-session-stickiness.test.ts. -
fix(test): replace the bare
expect(true).toBe(true)tautology inplayground-api-tab.test.tsx's SSE test and close thecheck:test-maskinggap that let it slip through for a full cycle (#6404) — a prior pass (#6548) had already swapped the literal toexpect(sendBtn).toBeDefined(), but that stayed just as vacuous: the test's fetch mock returned an empty/v1/modelslist, soApiTab's Send button is alwaysdisabled(!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, assertssendBtn.disabled === falsebefore 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 whenGITHUB_BASE_SHA/GITHUB_BASE_REFare 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 bareexpect(true).toBe(true)/assert.equal(1,1)/assert.strictEqual(1,1)patterns specifically (deliberately excludingassert.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(newscanBareTautologies/countBareTautologiescases) +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 sharedonPersistcallback for the manual "Refresh token" route, the reactive per-request refresh inchat.ts, and the Codex/Claude auth-file importers) correctly reused the storedrefresh_token, persisted the newaccess_token, and replaced a rotatedrefresh_token, but never cleared the staletestStatus/lastError*/errorCodefields 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 resetstestStatusto"active"and clearslastError,lastErrorAt,lastErrorType,lastErrorSource, anderrorCode(an explicittestStatusfrom 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/healthpayload — 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 onDELETE(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):
headroomcombo 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) viaexpandTargetsByQuotaAwareConnections, but discarded it before callinggetSaturation; for Codex,fetchCodexSaturationforwards straight tofetchCodexQuota(connectionId, connection), which needsconnection(or a priorregisterCodexConnection()call, which never happens before headroom ranking runs) to readaccessToken— so it returnednullfor every candidate, saturation failed open to0across the board, and ranking fell back to the original combo order regardless of actual free quota.getSaturation()and the headroomSaturationFetcherseam now accept and thread the loaded connection snapshot through tofetchCodexQuota. 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 #6150 —
freeProviderRankings.ts::getProviderModels()only ever walked the staticopen-sse/config/providerRegistry.tscatalog, so a user-added custom model (e.g. a Puterclaude-fable-5model 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-dupingmergeProviderModels()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/presetsunderREQUIRE_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-aino longer silently drops image/non-text content parts (#6390) —transformRequest()'sflattenContent()(added for #2539 to satisfy the Workers AI/ai/v1/chat/completionsplain-stringcontentrequirement) 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-levelchatCore.tscatch already routes throughbuildErrorBody()/sanitizeErrorMessage()(same pattern asbuildUrl()'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 thefractionReportedflag and defaulted an unreported model'sremainingPercentageto 0, which computed as 100% used; sinceconvertUsageToQuotaInfo()takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account intolimitReachedandquotaPreflightskipped it.percentUsedForQuota()now returnsnull(unknown, window ignored) wheneverfractionReported === false, before falling back toremainingPercentage. 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
chatCoresharing the client's headers, so they derived the sameIdempotency-Key/x-request-idand 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 onnexa/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/reasoningbefore forwarding the request (#6288) — Claude Code sendsreasoning_efforton every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400;transformRequest()'s existingUNSUPPORTEDsampling-param strip list (#5273) never covered it. Regression guard:tests/unit/grok-cli-reasoning-strip-6288.test.ts. -
fix(cli):
omniroute serveno longer hangs silently on a readiness timeout (#6321) — the child server's stdout was piped to"ignore"whenever--log/OMNIROUTE_SHOW_LOGwasn't set (the default), discarding any debug output, andrunWithSupervisor'swaitForServer(...).then((up) => { if (up) {...} })had noelsebranch, 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.tskept 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 newpricing_sync_statuskey_valuenamespace andgetSyncStatus()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/actionsfor Docker/LAN dashboard requests (#6277) — the route carried a duplicate per-routevalidateBrowserMutationOrigincheck re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline stripsPEER_IP_HEADERbefore 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
403from a no-credential (authType:"none") provider like mimocode or theoldllm no longer permanently bans the connection (#6315, #6345) —classifyProviderError()'s 403 branch only exemptedapikeyproviders from the terminalFORBIDDENclassification, so these free/stateless proxies (no real account/credential to revoke) fell through toFORBIDDENon the first unmatched 403 and gotisActive:false, testStatus:"banned"with no cooldown or retry. The exemption now also coversauthType:"none"providers, returningnull(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 exposesauggieas a.cmdshim, which Node'schild_process.spawncannot launch on win32 withoutshell: true. Both spawn sites (streaming + theauggie --versiontest) now go through a sharedbuildAuggieSpawnOptions()that setsshell: process.platform === "win32"; the argv (built bybuildAuggieArgs()with a registry-validatedmodeland 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) —
modelTestRunnersent its probe request without an explicit compression override, so whenever the operator's globalcompression.enabledflag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now sendX-OmniRoute-Compression: off, andchatCorehonors an explicitoffheader even whencompression.enabledis 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 aglobalThis-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. bygracefulShutdown/resetDbInstanceracing a reload); reusing that dead handle made the very next query throw sql.js's own bare string"Database closed"(not anError) straight out ofinstrumentation-node.ts's previously-unguardedensureDbInitialized()call, and Next.js's internalregisterInstrumentation()wrapper unconditionally doeserr.message = ...on whateverregister()rejects with — assigning.messageon a primitive string throws in strict mode, so the secondaryTypeErroris 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 newensureDbReadyForBoot()wraps the DB-init call, normalizes any non-Error throw vianormalizeBootError(), and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a realError. Regression guard:tests/unit/instrumentation-database-closed-6560.test.ts. -
fix(api):
POST /api/keysno longer hangs 20–90+ seconds on a fresh install, even with valid auth (#6570) —cloudEnableddefaults totrueinsrc/lib/db/settings.ts::getSettings()on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditionalawait syncKeysToCloudIfEnabled()always attempted a real outboundfetch()toCLOUD_URLviasyncToCloud(); 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.tsnow dispatchessyncKeysToCloudIfEnabled()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-syncfetch()is stubbed to never settle) + updatedtests/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'sprioritytoMAX(priority)+1per provider with no upper bound, and OAuth-imported connections (Codexcodex-auth/import/import-bulk, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass throughcreateProviderSchema's Zod validation at all, so nothing ever capped that value;EditConnectionModal'shandleSubmitalways resends the connection's currentpriorityunchanged on every save, andupdateProviderConnectionSchemacappedpriority/globalPriorityatmax(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 tomax(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
defaultModefield (surfaced asstrategy) instead of a nonexistentenginekey, and table output renders nested objects as JSON instead of[object Object](#6571 — thanks @charleszolot) -
fix(providers): web-cookie providers without a
providerRegistry.tsentry (lmarena,gemini-business,poe-web,venice-web,v0-vercel-web) now reportunsupported: trueinstead 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 toWEB_COOKIE_PROVIDERS[provider].websitewas proposed, but live verification showed the${website}/modelsprobe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API isarena.ai, notlmarena.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 returnsunsupported(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/hooksandPUT /api/middleware/hooks/[name]no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returnederror?.messagedirectly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route throughsanitizeErrorMessage()fromopen-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 rebuildinsidenode_modules/better-sqlite3, instead ofnpm rebuild better-sqlite3(#6700) — thebuilderstage installs dependencies withnpm 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. Invokingnode-gyp rebuilddirectly 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(...) }) withmain_modulemetadata; Cloudflare's Workers upload API parses a plainapplication/javascriptscript part as Service Worker syntax regardless of themain_modulemetadata field, andmain_modulerequires the script to actually be an ES module (top-levelexport), so the mismatch still rejected the upload (#6496).buildCloudflareWorkerScript()(src/lib/proxyRelay/cloudflareWorkerScript.ts) now emits Service Worker syntax (addEventListener("fetch", ...), no top-levelexport) and the upload metadata usesbody_partinstead ofmain_module. Regression guard:tests/unit/relay-deploy-5128.test.ts(asserts the emitted script has noexport default, registers afetchlistener, and the upload metadata carriesbody_part/omitsmain_module; also proves the inlinedisPrivateHostname()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) andurl…inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS usingmessage.metadata.content_references(#6635) —cleanChatGptText()now resolvescontent_references(grouped webpages, footnote sources, inlinewebpage/urlmentions) into[label](url)Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Prostream_handoffpolled-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 lightweightGET /api/health/pingliveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public (#6335) —clientApiPolicyrequired a bearer/dashboard-session before the WS route handler could even return its ownwsAuth/protocol descriptor, and/api/health/pingwas never added toPUBLIC_READONLY_API_ROUTE_PREFIXESdespite 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/pingis now inPUBLIC_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, andtests/unit/authz/classify.test.ts(/api/health/pingclassifiedPUBLIC). (thanks @JxnLexn) -
fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing
julescloud-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(noindex) so tool-call arguments are no longer split/lost in request/response logs (#6276) -
fix(providers): modernize the
lmarenaprovider for the Arena.ai rebrand — route chat througharena.aicreate-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep thelmarena/lmawire 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-authcookie 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=0webpack 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_tokencapability 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 topublic/images/so Next.js serves them (#6538 — thanks @ianriizky). -
fix(routing): the
autocombo's no-auth candidate pool now honors a disabled provider connection's ownisActive=false(the toggle on the main Providers grid card), not just the separate globalblockedProviderssetting — 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 andtool_choiceinstead 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.jsstmt.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 inusage_historytoo, 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_tokensto 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_turnfinish 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
getExplicitModelOutputCapfalls through to the registry/spec cap when a synced capability row exists without a numericlimit_output(#6714) — thanks @xz-dev -
fix(api):
omniroute health(andhealth components/health watch) returnedError: HTTP 404(#6677) —bin/cli/commands/health.mjscalledapiFetch("/api/health", ...), a route that was moved toGET /api/monitoring/health(src/app/api/monitoring/health/route.ts) without updating the CLI;src/app/api/health/on disk only hasdegradation/route.tsandping/route.ts, no top-level handler.runHealthCommand()/runHealthComponentsCommand()now call/api/monitoring/healthand read its actual payload shape (activeConnections,circuitBreakers: {open, halfOpen, closed},memoryUsage) instead of the old, nonexistentrequests/breakers/cache/memoryfields. 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) andreasoningControls.ts(the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless formfrom "./reasoningControls", the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module toreasoningControlUtils.ts(no collision) and updated the 3 import sites. Regression guard:tests/unit/case-collision-6584.test.ts(scanssrc//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'soutputBaseis built aspath.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir), whereoutputDiris a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamicreaddirSync/rmSync/readFileSync/writeFileSynccall 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'sturbopack.ignoreIssue(Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existingwebpack.ignoreWarnings/isNextIntlExtractorDynamicImportWarningprecedent already in the same file for the webpack path. Regression guard:tests/unit/next-config.test.ts(asserts theturbopack.ignoreIssuerule shape targetingsrc/lib/agentSkills/**). -
fix(providers): Codex Desktop requests to
gpt-5.3-codex-sparkfailed 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-injectedimage_generationhosted tool whenisCodexFreePlan()matched the account's plan, with no awareness that Spark-scope Codex models rejectimage_generationupstream regardless of plan.dropImageGenerationnow also drops it whengetCodexModelScope(model) === "spark"(the existing Spark classifier fromopen-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'ssortQuotasByRemaining()(added in #5977) was applied unconditionally viauseMemo(() => sortQuotasByRemaining(quotas), [quotas]), undoing the deterministicCODEX_QUOTA_ORDER/GLM_QUOTA_ORDERwindow orderquotaParsing.ts'ssortCodexOrder()/sortGlmOrder()(added in #6336) already established for Codex and the GLM family — since #6336 never touchedQuotaCardExpanded.tsx, the two orderings never composed, so e.g. a Codexsessionwindow with less headroom thanweeklyrendered after it instead of staying first. A newhasFixedQuotaOrder()(quotaParsing.ts) andresolveQuotaDisplayOrder()(QuotaCardExpanded.tsx) now skip the remaining-% re-sort for providers with a fixed window order, threadingproviderIdfromQuotaCard.tsxthrough 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 9providers.*keys are disjoint, present-but-untranslated sentinels caused by a separateproviderText()fallback bug) (#6695) —scripts/i18n/sync-ui-keys.mjs(which mirrors newly-addeden.jsonkeys into every locale) wasn't re-run after recenten.jsonadditions, and the CIi18n:check-ui-coveragegate 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 intosrc/i18n/messages/pt-BR.json(translated to Brazilian Portuguese, no leftover__MISSING__markers) vianpm run i18n:sync-ui -- --locale=pt-BR+ manual translation. Regression guard:tests/unit/i18n-pt-br.test.ts(new case asserting fullen.json→pt-BR.jsonkey parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). -
fix(startup):
omniroute --mcpcrashed at Node ESM link time withERR_MODULE_NOT_FOUNDforioredison installs where the published MCP bundle didn't happen to haveioredisrescued from a parentnode_modules(#6559) —src/shared/utils/rateLimiter.tshad a top-level staticimport Redis from "ioredis"; that module is only ever reached via a lazyawait import(...)several call-sites deep in the MCP tool chain, but esbuild's--packages=externalbundling of the MCP server (scripts/build/prepublish.tsStep 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compileddist/open-sse/mcp-server/server.js, forcing Node to resolveioredisat module-link time — before any--mcpstartup code runs — andioredisis not guaranteed to ship in the MCP-only bundle'snode_modules.getRedisClient()now lazily importsioredison first use (matching the established soft-dependency pattern insrc/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 staticioredisimport remains, while the pre-existing lazyawait import("ioredis")inredisQuotaStore.tsstays intact). -
fix(providers): Kiro sent the adaptive-thinking
additionalModelRequestFieldsenvelope forclaude-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-APIsupportsReasoning()capability flag, which istruefor both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; onlyclaude-sonnet-5is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (supportsKiroAdaptiveThinking()inopen-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'sReadtool only, so Cursor'sSubagenttool passed through with the cloud-onlycloud_base_branch: ""(Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist ofRead+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
idandfunction.namein separate SSE chunks) no longer produce an empty tool name /No such tool available:error through the Claude/messagespath — theopenai-to-claudestreaming translator emittedcontent_block_startimmediately on the id-only chunk with an emptyname, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now deferscontent_block_startuntil the tool name arrives (falling back to starting the block when arguments arrive first), so the emittedtool_usealways 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, andparseRetryFromErrorText()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 newshouldPreserveQuotaSignals()(open-sse/services/quotaResetParsing.ts), andparseDayGranularityResetMs()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 alignedtests/unit/account-fallback-service.test.tscases 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), butexpandTargetsByFingerprints()(open-sse/services/combo/fingerprintExpansion.ts) looked that composite string up directly inconnectionById(keyed by real DB row ids), gotundefined, 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 (newsplitFingerprintPin()helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the newpinnedFingerprintfield) 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'ssse.json()parser (#6561), follow-up to #6199/#6232 — the commentary-dropcontinue;branches inopen-sse/utils/stream.tsskipped thedata:line for a dropped commentary event but never cleared the already-bufferedevent:line for that same frame, so the next blank line flushed the staleevent:line alone. Both drop sites now callclearPendingPassthroughEvent()beforecontinue, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a newopen-sse/utils/responsesCommentaryDrop.tsso the fix does not grow the frozenstream.ts. Regression guard:tests/unit/responses-commentary-event-frame-6561.test.ts(realisticevent:\ndata:\n\nframes — the existing #6199 test only used baredata:lines and never exercised this path). -
fix(compression):
/api/compression/preview's top-leveloriginalTokens/compressedTokensdiverged fromengineBreakdown[0]'s counts for the same single-engine run (tiktoken outer counts vs theJSON.stringify(...).length/4estimate per engine), worst on small inputs. A newreconcileSingleEngineTokens()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 upstreamQuotaFetcher. OnlygetProviderCredentialsWithQuotaPreflight()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 throughgetProviderCredentialsWithQuotaPreflight()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/completionsSSE on theclaude-webandv0-vercel-webexecutors (#6662) — every chunk builder in both adapters hardcodeddelta: { content: ... }with no reasoning path, unlike the established pattern already used bydefault.ts/deepseek-web.ts/bedrock.tsand the real-Anthropic-APIclaude-to-openai.tstranslator (thinking_delta→reasoning_content).v0-vercel-web.tsnow forwards an upstreamdelta.reasoning_contentfield (streaming and non-streaming) the same waydeepseek-web.tsdoes.claude-web.ts'sbuildClaudeStreamingResponsenow maps acontent_block_start(type: "thinking")/content_block_delta(delta.thinking) pair ontodelta.reasoning_content, andclaude-web/payload.ts'stransformToClaude()no longer hardcodesthinking_mode: "off"— a newwantsExtendedThinking()derives it from the request'sreasoning_effort/reasoning.effort/thinking.typesignal, 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
enableRenderersfor 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
apiKeylength 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*.ymlmanifests 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.0to0.144.0for 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, buttests/unit/dashboard/**requires the--import tsxCJS transform (ESM-only deep imports like@lobehub/icons/es/*), exactly as the canonicaltest:unit:ci:shardalready does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). -
fix(translator): read PDF/video
file_dataattachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map todocument/inlineDataand videos keep theirvideo/mp4mime 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
PUTschema (stackedPipelineStepSchema) now accepts everyENGINE_CATALOGid — the structural enginessession-dedup/ccr/headroom/relevance/llmlingua/omniglyphand theaggressiveultraintensity — so aGET→PUTround-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-versionso 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 ownGET /api/usage/requests-by-provider-dateroute (kept separate from the frozen/api/usage/analyticsroute). 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 byshouldUseQuotaSignal, which is oauth-only, so apikey providers likeollama-cloudfell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A newisWeeklyUsageLimitText/buildWeeklyQuotaFallbackclassifier (extracted, with the existing subscription-quota logic, into a newopen-sse/services/quotaTextCooldowns.tsmodule so the frozenaccountFallback.tsdidn't have to grow) runs unconditionally and applies a 24hQUOTA_EXHAUSTEDcooldown regardless of provider category. Regression guard:tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts. -
fix(providers): an explicit
thinking.budget_tokens: 0is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). -
fix(bootstrap): filter empty
process.envvalues 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
404responses asMODEL_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_COUNTby tool name instead of double-counting collections (#6854) -
fix(usage): xAI's exact provider-reported
cost_in_usd_ticksno longer silently acceptsnull/""/negative values as a valid$0exact cost —extractUsageFromResponse()(open-sse/handlers/usageExtractor.ts) andnormalizeUsage()(open-sse/utils/usageTracking.ts) now requiretypeof value === "number" && Number.isFinite(value) && value >= 0instead of coercing withNumber(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-plugindynamic provider hook stopped embedding its OC-1.17.8+-gate-compatibleopencode--prefixed provider id into model routing fields (ModelV2.id/providerID, combo catalog keys) — OmniRoute's server has noopencode-<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.completeduntil a trailing usage-only chunk arrives on/v1/responsesstreams (#6906) -
fix(cli): ship
head-response-guard.cjsinto the standalone bundle —server-ws.mjsimported it without a matchingEXTRA_MODULE_ENTRIESentry, so everybuild:releasedist crashed at boot withERR_MODULE_NOT_FOUND; a new regression test derives the required sidecars fromserver-ws.mjsimports (#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_tokenstomax_tokensfor 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
RelayTokenintocheckRateLimit, skipping a redundantSELECT * FROM relay_tokens WHERE id = ?re-query on every request (#6930) -
fix(sse):
normalizeCodexMessageContentPartnow rewrites explicittype: "input_text"(not justtype: "text") tooutput_texton assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli asinput_textis no longer rejected by the Codex/OpenAI backend (#6932) -
fix(sse): omit removed
attachmentsfield from Muse Spark Web (Meta AI) persisted GraphQL query to fixUnknown 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->geminitransform now mapsreasoning_effort: "none"tothinkingConfig.thinkingBudget: 0(withincludeThoughts: 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.providerbefore checkingROTATING_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 justRead/Subagent) viaschema.required, and thread each tool's schema from the request'stools[]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
isChatCapablegate inaddModelOption()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-2508aliases 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-anyseverity 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-exportedrelayProbeStats(db-rules), documentedOMNI_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 forPOST /api/github-skills; the missingomni-github-skillsregistry 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-greensuppressions-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-nocheckdirective fromopen-sse/utils/usageTracking.ts(#6173) —tscunder the standardtypecheck:coregate 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) inbin/cli/commands/completion.mjsno longer swallow errors into a barecatch {}(#6257) — each now binds the error and, when the newOMNIROUTE_DEBUG_COMPLETIONenv var is set, emits a[omniroute completion]diagnostic tostderr; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) -
chore(quality):
validate-release-green --full-cireproduces the fullci.ymlstatic gate set locally — the pre-flight now readsci.ymlitself and runs everynpm run check:*from thelint/quality-gate/quality-extended/docs-sync-strict/pr-test-policyjobs (--ratchet flags preserved,test-maskingagainstGITHUB_BASE_REF=main), skipping only the non-localpr-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 intonightly-release-greenso a static base-red opens a tracking issue the night it lands. Regression guard:tests/unit/validate-release-green.test.ts(+5extractCiGatescases). -
refactor(usage):
saveRequestUsage(entry: any)is now typed with a newUsageEntryinterface mirroring theusage_historycolumns 1:1 (#3512) — the other strayanys insrc/lib/usage/usageHistory.ts(getUsageHistoryfilter, thegetUsageDbnext-cursor cast,appendRequestLog's legacytokensparam,getRecentLogs's catch) were cleaned in the same pass, so the file now sits in thecheck:any-budget:t11zero-anyallowlist. The DB-entity ↔ TS-interface convention is documented indocs/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--adminevidence 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.mjsnow unions the CHANGELOG scan window withchangelog.d/fragment refs (filename<PR>-prefix + every#Nin 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_ONkeeps 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
p2ccasing fixed to matchROUTING_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
pipelinerow), 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.tsinstryker.conf.jsontap.testFiles(mutation-coverage gap left by #6903) and updateprovider-models-route-codex.test.tsto expect Codex client version0.144.0(stale assertion left by #6780's production bump). -
docs: refresh
llm.txtto 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 todocs/architecture/DESIGN_SYSTEM.mdrewritten as a reference doc. -
chore(security): scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the
tests/boundary/*.live.test.tsfiles landed via #6786 — they now readOMNIROUTE_TEST_BASE/OMNIROUTE_TEST_BEARER/OMNIROUTE_TEST_COOKIEfrom the environment and stay gated behindRUN_BOUNDARY_LIVE=1.
🙌 Contributors
Thanks to everyone whose work landed in v3.8.47: