✨ New Features
- feat(sse): hide paid-only models from
auto/*routing whenhidePaidModelsis on (#6512) — follow-up to #6328/#6495. PR #6495 hid paid-only models from theGET /v1/modelslisting, butauto/*combos (auto/best-coding,auto/glm, …) could still pick a paid-only backend into their candidate pool → a 402/403 at request time.createVirtualAutoCombonow filters the candidate pool through the new pureopen-sse/services/autoCombo/paidModelFilter.ts(filterPaidOnlyCandidates), applying the same free-model predicate #6495 uses incatalog.ts(providerHasFreeModels(provider) && isFreeModel(provider, {id})) wheneversettings.hidePaidModels === true. Applied before the category/tier/family narrowing, so it covers everyauto/*combo; an all-paid pool degrades to the existing graceful empty-pool path. Opt-in — default OFF leaves the pool unchanged (identity). Regression guard:tests/unit/autoCombo/paid-model-filter-6512.test.ts(4, incl. the default-off identity guard). - feat(sse): provider-family auto combos —
auto/glm,auto/minimax,auto/mimo,auto/zai,auto/gemma,auto/llama,auto/gemini(#6453) — new routable ids that materialize an on-demand virtual combo spanning whatever installed backends currently expose that model family, degrading gracefully as backends rotate. A new pureopen-sse/services/autoCombo/modelFamily.ts(detectModelFamily) classifies by model-id prefix for six families;zaiis instead resolved by provider id (z.ai's hosted API serves the sameglm-*model ids as every other GLM backend, soauto/zaimeans "route to my z.ai backend specifically" vsauto/glm's "any connected GLM backend"). Reuses the existingcreateVirtualAutoComboon-demand materialization path (no DB writes) and the/v1/modelscatalog advertising loop. Regression guard:tests/unit/autoCombo/provider-family-combos.test.ts(11). - feat(proxy): native proxy-pool round-robin / egress IP rotation (#6365) — a scope (global / provider / account) can now hold multiple proxies as a pool with a rotation strategy, so outbound requests cycle their egress IP instead of pinning one proxy per scope. Migration
117_proxy_pool_rotation.sqllifts theUNIQUE(scope, scope_id)constraint (rebuild via the canonical rename/copy/drop; existing single assignments become 1-element pools) and adds aproxy_scope_rotationcompanion table holding the per-scope strategy + a persisted monotonic round-robin cursor. Strategies:round-robin(default, monotonic cursor — neverMath.random),random, andsticky-per-N-min. Resolution (resolveProxyForScopeFromRegistry/resolveProxyForConnectionFromRegistry) now fetches the alive, position-ordered candidate set (unchangedPROXY_ALIVE_PREDICATE) and applies the strategy; an empty / all-dead pool still returnsnull— the #6246 fail-closed guard is untouched (never falls through to direct egress). Backend + DB only; dashboard pool-builder UI is a follow-up. Regression guard:tests/unit/proxy-pool-rotation-6365.test.ts(8, incl. fail-closed + backward-compat). - feat(providers): end-to-end tool/function calling on the native Gemini
/v1betaendpoint (#6222) — both directions of the Gemini↔OpenAI conversion now preserve tool data (previously silently dropped). Request side:convertGeminiToInternal(extracted to its own testable module) mapstools[].functionDeclarations→ OpenAItools, priorfunctionCallparts → assistanttool_calls, andfunctionResponseparts →tool-role messages. Response side:convertOpenAIResponseToGeminiemitsparts[].functionCall {name,args}frommessage.tool_calls, and the streamingopenAIChunkToGeminiChunkaccumulates fragmentedtool_callsdeltas by index into completefunctionCallparts. The non-Gemini client paths (Claude, OpenAI-Responses) already preserved tool calls — this closes the gap specific to the native Gemini surface. Regression guard:tests/unit/v1beta-gemini-tool-calling-6222.test.ts(6, incl. a streaming SSE round-trip). - feat(providers): copilot-m365-web enterprise / work tier support (#6334) — mirrors the EDU-tier pattern (#6210):
M365ConnectionParamsgains anagentfield, a new opt-inM365_ENTERPRISE_OVERRIDESpreset (agent=work,scenario=officeweb,licenseType=Premium) applies viaproviderSpecificData.tier="enterprise"(alias"work"), andagentis also overridable directly viaproviderSpecificData.agent.buildWsUrlwas hardcodingagent="web"(the one enterprise-distinguishing param with no override path), so a Premium work account handshook then returned an empty stream. The individual and EDU paths are untouched. Kilo's dup flag vs #6210 (EDU tier) was a false positive — different tier. Regression guard:tests/unit/copilot-m365-enterprise-6334.test.ts(7). End-to-end confirmation on a real Premium work account is a live-VPS validation follow-up (Hard Rule #18). (thanks @Forcerecon) - feat(api): standardized, provider-agnostic
effort+thinkingrequest params (#6241) — a thin standardization layer over the existing mature per-provider reasoning plumbing (no provider mapper touched).providerChatCompletionSchemagains a canonicaleffort(reusing the sharednone/low/medium/high/xhighvocabulary — the UI tiersextra/maxcollapse ontoxhigh) and a booleanthinking. A purenormalizeReasoningRequest(wired once insrc/sse/handlers/chat.ts, before any reasoning field is read) folds them onto the fields the translators already consume (reasoning_effort/reasoning.effort/thinking), so they fan out to Anthropic / Gemini / xAI / Responses — an explicit clientreasoning_effort/ object-shapedthinkingalways wins (backward-compatible)./modelsadditively exposessupportsThinking+effort_tiersso the frontend can render the toggles (UI component is a follow-up). Regression guard:tests/unit/effort-thinking-standardization-6241.test.ts(12). (thanks @Iammilansoni, @shabeer) - feat(combo): new
pipeline(sequential) combo strategy (#6297) — the 18th routing strategy runs targets in order, threading each step's output into the next step's input, with an optional per-stepprompt(system instruction); only the final step's response is returned. Distinct fromfusion(parallel fan-out + judge). Implemented as a self-containedopen-sse/services/pipeline.ts(sibling tofusion.ts), dispatched fromcombo.ts; the step list reusescombo.modelsorder and reads an optionalpromptoff each target (backward-compatible — ignored by every other strategy). Intermediate steps run non-streaming with tools stripped (complete prose to thread forward); the final step keeps the client'sstreamflag + tools. A failing/empty/unparseable intermediate step fails the whole pipeline explicitly via a sanitized error (never silently swallowed). Kilo's dup flag vs #563 was a false positive (that's model→chain selection; this is a sequential chain). Regression guard:tests/unit/combo-pipeline-strategy.test.ts(5). (thanks @ofekbetzalel) - feat(ci):
check:test-maskingnow flags inline-reimplemented prod conditions (#6348) — a new report-only subcheck (v2, 6A.10 family) catches the wrong-shape contract test: a test that recomputes the condition under test inline instead of importing/exercising the real function (the #6216 class, where=== 500→>= 500stayed green because the test re-implemented the branch). For each added/modified test file it warns when the file textually duplicates a ≥3-token conditional from a production file touched in the same PR and does not import the symbol/module owning it, via a pure, fixture-testedfindReimplementedConditions()with an allowlist mirroringassertReductionAllowlist. Report-only for now (does not fail the gate) — to be promoted to blocking after a triage cycle. Regression guard:tests/unit/check-test-masking.test.ts(45). - feat(sse): per-connection routing override (native vs CLIProxyAPI) (#6339) — the previously-dead
isCliproxyapiDeepModeEnabledhelper is now wired intoresolveExecutorWithProxy: a single connection can opt itself into the CLIProxyAPI passthrough executor viaproviderSpecificData.cliproxyapiMode="claude-native", with precedence connection override > providerupstream_proxy_configmode > default.resolveExecutorWithProxynow receives the resolved connection'sproviderSpecificData(threaded fromchatCore.ts), so one connection can deep-route while the provider's other connections stay native — no DB schema change (the toggle rides inproviderSpecificData). Also resolves the same-provider mixing ask in #6340. Regression guard:tests/unit/chatcore-executor-proxy.test.ts(9). (thanks @RaviTharuma) - feat(dashboard): "Add session cookie" modal now shows a prominent "Open ‹host› →" link to the provider's own site (#6268) — every
-webcookie-session provider (chatgpt-web, claude-web, gemini-web, kimi-web, lmarena, qwen-web, m365-copilot-web, …) renders a one-click external link (opening the provider's login/home page in a new tab) so operators no longer tab away to retype the URL mid-setup. The host resolves from a pure, unit-testedresolveWebProviderHost()(prefersWEB_COOKIE_PROVIDERS[id].website, falls back to the registrybaseUrlorigin); non-web providers render exactly as before. Kilo's dup flag vs #6265 (modal-too-small-on-1080p) was a false positive — distinct concern. Regression guard:tests/unit/resolve-web-provider-host.test.ts(5). (thanks @chirag127) - feat(providers): add DigitalOcean AI (serverless inference) as an OpenAI-compatible API-key provider (#6373) — base
https://inference.do-ai.run/v1, wired through the shared OpenAI-compatible registry with full model passthrough (open-sse/config/providers/registry/digitalocean/,src/shared/constants/providers/apikey/inference-hosts.ts). Regression guard:tests/unit/digitalocean-provider.test.ts. (thanks @newnol) - feat(providers): add Huancheng Public API (
hcnsec) as an OpenAI-compatible regional provider (#6410) — Xinjiang Huancheng Cybersecurity's public LLM platform (basehttps://api.hcnsec.cn/v1, free credits via daily check-ins), wired through the shared OpenAI-compatible registry with full model passthrough (open-sse/config/providers/registry/hcnsec/,src/shared/constants/providers/apikey/regional.ts). Regression guard:tests/unit/hcnsec-provider.test.ts. (thanks @UnrealAryan) - feat(dashboard): the web-session credential guide now shows an "Open {host}" link (#6316) to the provider's sign-in site (derived from the provider
websiteviagetProviderWebsiteHost), so you can jump straight to the page where the cookie/session must be captured. Regression guard:tests/unit/web-session-provider-link-6316.test.ts. (thanks @jordansilly77-stack) - feat(cerebras): add the Gemma 4 31B model (
gemma-4-31b) to the Cerebras registry + pricing table (#6331). Regression guard extendstests/unit/t28-model-catalog-updates.test.ts. (thanks @backryun) - feat(providers): add Yuanbao (web) as a cookie-session provider (#6196) —
yuanbao-web(Tencent Yuanbao,yuanbao.tencent.com) with cookie-only auth (hy_user/hy_token+ public agent id), SSE→OpenAI translation incl.reasoning_content, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard:tests/unit/providers-yuanbao-web.test.ts.together-webwas deferred (no verifiable web-session endpoint — needs a captured request) andhuggingchat-webdropped (the existinghuggingchatalready is a web-cookie provider). (thanks @chirag127) - feat(providers): route the built-in agentrouter through the dynamic Claude-Code wire image (#6056) — a small static allow-set (
CC_WIRE_IMAGE_BUILTINSinopen-sse/services/ccWireImageBuiltins.ts), consulted byisClaudeCodeCompatible/isClaudeCodeCompatibleProvider/applyFingerprint, makes agentrouter adopt the CC wire-image headers + fingerprint while guarding the CC baseUrl/auth branches so it keeps its own registrybaseUrlandx-api-keyauth. Regression guard:tests/unit/agentrouter-cc-wire-image.test.ts(asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18). - feat(providers): bulk-add API keys for Cloudflare Workers AI (#6174) —
cloudflare-aiis removed from the bulk-add exclusion list and the bulk parser gains a 3-fieldname|accountId|apiKeymode; the bulk route now builds a per-entryproviderSpecificDataso each key carries its ownaccountId(fixing the previous shared-object reuse), and both the create + key-validation paths receive it. Regression guard:tests/unit/bulk-api-key-parser-cloudflare.test.ts. (thanks @muflifadla38) - feat(dashboard): routing/settings UX clarity (#6147) — (1) weighted combos show the effective routing share % next to each weight when weights don't sum to 100 (
WeightTotalBar.tsx); (2) the status widget's user-facing "Cloud Sync" label is renamed to "Remote Settings Sync" (CloudSyncStatus.tsx; internal ids/state untouched); (3) built-in providers gain an opt-in advanced base-URL override (isBaseUrlOverrideEligibleProvider, hidden behind an "Advanced" toggle, reusing the existingproviderSpecificData.baseUrlpersistence — not globally widened). Regression guard:tests/unit/routing-settings-ux-6147.test.ts. - feat(combo): add an option to disable session stickiness, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo
config.disableSessionStickiness→ globalsettings.disableSessionStickiness→ defaultfalse(preserves the #3825 prompt-cache/504 fix); gates both stickiness call sites inopen-sse/services/combo.ts. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. (#6168) Regression guard:tests/unit/combo-disable-session-stickiness.test.ts. (thanks @RCrushMe) - feat(docker): add the
OMNIROUTE_NO_SUDOenv flag for root-less / user-namespaced deployments — the MITM cert-trust command path (resolveSudoSpawninsrc/mitm/systemCommands.ts) now strips the leadingsudowhen the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs withoutsudo(the operator trusts the CA manually, e.g. viaNODE_EXTRA_CA_CERTS). Argv-arrayspawnpreserved — no shell interpolation (Hard Rule #13). (#6122) Regression guard:tests/unit/mitm-systemCommands-no-sudo.test.ts. (thanks @powellnorma) - feat(providers): add Requesty as an OpenAI-compatible gateway provider (BYOK, base
https://router.requesty.ai/v1, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (open-sse/config/providers/registry/requesty/,src/shared/constants/providers/apikey/gateways.ts). (#6120) Regression guard:tests/unit/requesty-provider.test.ts. (thanks @chirag127) - feat(dashboard): add configured-only / available-only filters to the Free Provider Rankings page (#6150) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (
?configuredOnly/?availableOnlyonGET /api/free-provider-rankings) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard:tests/unit/freeProviderRankings-filters.test.ts.
🔧 Bug Fixes
-
fix(dashboard): adding a second API key connection for the same provider no longer silently overwrites the first — the Add-API-key modal now derives a unique default connection name (
main, thenmain-2,main-3, …) so the backend name-based upsert can't collide (#6499 — thanks @dilneiss). -
fix(compression): the session-dedup engine now also deduplicates a large multi-line block repeated within a single message (intra-message dedup), not just across turns; the compression-preview API surfaces a
fallbackReason, and the fusion panel reports how many models were rate-limited vs failed on total-panel failure (#6501 — thanks @chirag127). -
fix(compression): a stacked-pipeline step naming an unregistered engine now surfaces a
validationErrorsentry instead of silently no-op'ing, so misconfigured pipelines are visible in the preview API (#6506 — thanks @chirag127). -
feat(usage): add a Codex reset-credit redemption flow to the Provider Limits UI (#6361) — a
useCodexResetCreditRedemptionhook +/api/usage/codex-reset-creditroute +codexResetCreditslib let you redeem banked Codex reset credits from the quota card. Regression guard:tests/unit/codex-reset-credits.test.ts. (thanks @JxnLexn) -
feat(glm): add team-plan quota settings for
glm-cnconnections (#6351) — a dedicatedGlmTeamQuotaFieldsform section (team quota id / limits) threaded through the Add/Edit connection modals, persisted viaproviderSpecificData, with the GLM usage service reading the team quota. Regression guards:tests/unit/glm-team-quota.test.ts,provider-specific-data-schema.test.ts. (thanks @hao3039032) -
feat(providers): add TinyFish web-fetch/search support (#6349) — a
tinyfish-fetchexecutor +/v1/web/fetchroute + MCP web-fetch tool, registered as a specialty-media provider with request-validation and a search-provider catalog entry. Regression guards:tests/unit/executor-tinyfish-fetch.test.ts,web-fetch-handler.test.ts,mcp-web-fetch-tool.test.ts,provider-validation-tinyfish.test.ts. (thanks @dtybnrj) -
fix(cli):
omniroute launch-codexnow spawnscodex.cmdthrough a shell on Windows (the npm.cmdshim is unresolvable by barespawn→ ENOENT), mirroring the qodercli Windows fix (#6263) (#6312). Regression guard:tests/unit/launch-codex-windows-spawn-6312.test.ts. (thanks @swingtempo) -
fix(codex): isolate the Spark quota from the shared Codex quota and stabilize the quota UI ordering / hydration so per-scope limits render consistently (#6336). Regression guards:
tests/unit/codex-quota-selection-hydration.test.ts,provider-limits-ui.test.ts+ 3 more. (thanks @xz-dev) -
feat(api): add a
hidePaidModelssetting that filters paid-only models out of the/v1/modelscatalog. Regression guard:tests/unit/models-catalog-hide-paid.test.ts. (thanks @chirag127) -
fix(api-manager): the fallback model picker now preserves combos instead of dropping them when a primary model is unavailable (#6443). Regression guard:
tests/unit/api-manager-page-static.test.ts. (thanks @jmengit) -
fix(providers): recoverable Antigravity / Cloud-Code (Gemini Code Assist)
403responses (#6452) are now classified as a retryable project-config error instead of a terminal account ban, so a fixable project/API-disabled 403 no longer forces a ~1-year cooldown / full OAuth reconnect. Regression guard:tests/unit/errorclassifier-antigravity-403.test.ts. (thanks @developerjillur) -
fix(mitm):
sanitizeHeadersnow redactsSet-Cookieresponse headers so upstream session cookies never leak into logs / diagnostics (#6451). Regression guard:tests/unit/mitm-sanitize-headers.test.ts. (thanks @developerjillur) -
fix(api):
/api/compression/previewnow acceptsmode: "caveman"and correctly handles stacked / zero-compression previews (#6425). Regression guard:tests/unit/api/compression-preview-caveman-and-stacked-6425.test.ts. (thanks @chirag127) -
feat(providers): add Zed hosted LLM aggregator as a native-app provider (#6118) — OAuth sign-in via the Zed hosted flow, registered through the shared provider registry + executor. Regression guards:
tests/unit/zed-oauth-provider.test.ts,zed-import-utils.test.ts,zed-docker-detect.test.ts,mitm-handler-zed.test.ts. VPS-validated via live operator login (Hard Rule #18). -
fix(oauth): the Kiro SSO-cache auto-import now preserves the IDC region — cross-region Amazon Q / Kiro profiles imported from the SSO cache are no longer collapsed to the default region (#6113). Regression guard:
tests/unit/kiro-auto-import-idc-2059.test.ts. VPS-validated via live operator login (Hard Rule #18). -
fix(dashboard): passthrough model aliases no longer collide when two namespaced model ids share a last segment (port from 9router#1850, #6431).
enx/gpt-5.5andenx/codebuddy/gpt-5.5both auto-generated the aliasgpt-5.5, so the second model could never be added (the UI just alerted "alias already exists"). Aliases are now disambiguated deterministically — bare last segment when free, then parent-qualified (codebuddy-gpt-5.5), then a numeric suffix — while re-adding the exact same model id is still blocked. Regression guard:tests/unit/passthrough-alias-1850.test.ts. (thanks @arpicato) -
fix(translator): preserve a Gemini
functionResponseco-located with other parts (anotherfunctionCall, or trailingtext) in the same content when translating Gemini → OpenAI (#6376).convertGeminiContent()early-returned the tool message on the firstfunctionResponsepart, dropping any co-located parts; such contents are now pre-split (one tool message perfunctionResponse, emitted first, plus one message for the remaining parts). Regression guard:tests/unit/gemini-to-openai-function-response.test.ts. (thanks @warelik) -
fix(headroom): detect a python interpreter managed by mise / pyenv / asdf / conda (port from 9router#2353, #6382). Headroom's python probe (
src/lib/headroom/detect.ts) searched a hardcodedPATH, but version managers expose their interpreters via shim dirs that only joinPATHthrough interactive-shell activation — which the non-interactive server never runs, so a managed python (≥3.10) was invisible and Headroom reported it missing. The search path now prepends the well-known shim/bin dirs (~/.local/share/mise/shims,~/.pyenv/shims,~/.asdf/shims,$CONDA_PREFIX/bin,~/.local/bin, respectingMISE_DATA_DIR/PYENV_ROOT/ASDF_DATA_DIRwhen set), and a newHEADROOM_PYTHONenv override lets operators point straight at their interpreter (mirroringHEADROOM_URL). Still shell-free (execFileSync). Regression guard:tests/unit/headroom-detect.test.ts(5). (thanks @loopyd) -
fix(executors): strip the OpenAI-Codex/Claude-CLI
client_metadatapassthrough field for NVIDIA requests (port from 9router#1887, #6411). NVIDIA's OpenAI-compatible wrapper rejects it with400 Unsupported parameter, the same class already handled forcerebras/mistral;nvidia(executordefault) was missing from the strip allowlist so Codex/Claude-Code passthrough requests 400'd. Regression guard:tests/unit/executor-default-strip-client-metadata.test.ts(+nvidia case). (thanks @phidinhmanh) -
fix(translator): strip the Claude-style
thinkingfield for NVIDIAz-ai/glm-5.2(port from 9router#2023, #6413). NVIDIA's OpenAI-compatible wrapper 400s onthinking(a Claude-format client routed here leaves athinking:{type:"adaptive"}); the existing strip rule only droppedreasoning. Same class already handled forminimax-m2.7. Regression guard:tests/unit/nvidia-minimax-thinking-strip.test.ts(+glm-5.2 case). (thanks @phidinhmanh) -
fix(translator): suppress the streamed
</think>close marker for the Antigravity IDE client (port from 9router#1061, #6415). On thinking-only turns Antigravity rendered a bare</think>as the sole visible content, tripping its loop-detection and wasting requests. Antigravity's UA (vscode/<v> (Antigravity/<v>)) is added to the marker-suppress allowlist (alongside OpenCode); Claude Code / Cursor still get the marker, andx-omniroute-thinking-marker: onforce-restores it. Regression guard:tests/unit/think-close-marker-suppress-5245.test.ts. (thanks @abdofallah) -
fix(executors): strip nested
reasoning_contentfrom messages for Mistral (port from 9router#1649, #6417). Mistral's API returns422 extra_forbiddenwhen an assistant message carriesreasoning_content(replayed thinking from a prior turn, e.g. via the Codex/responsespath); the generic top-level 400 field-downgrade retry never covered the nested per-message field.DefaultExecutornow strips it for providermistralonly, so DeepSeek (which requires replayedreasoning_content) is unaffected. Regression guard:tests/unit/mistral-strip-reasoning-content-1649.test.ts. (thanks @xxy9468615) -
fix(executors): strip the
client_metadatapassthrough field on the OpenCode path (port from 9router#1442, #6418). OpenCode upstreams (e.g.kimi-k2.6via opencode-go) reject it with400 "Extra inputs are not permitted, field: 'client_metadata'"; the DefaultExecutor strip only covered cerebras/mistral andOpencodeExecutorextendsBaseExecutordirectly, so nothing removed it there. Regression guard:tests/unit/opencode-strip-client-metadata-1442.test.ts. (thanks @yanpaing007) -
fix(executors): inject the
reasoning_contentecho for the native Moonshot Kimi provider (port from 9router#1480, #6419). Kimi (executordefault) is a thinking-mode upstream that 400s with "reasoning_content must be passed back" when a prior assistant turn lacks it; the placeholder injection was only wired into the OpenCode meta-provider, so direct multi-turn Kimi conversations failed. Scoped tokimi(gateway-served models matching the thinking-model name pattern are unaffected). Regression guard:tests/unit/kimi-native-reasoning-injected-1480.test.ts. (thanks @2220258345) -
fix(executors): recover from a strict gateway's
context_management: Extra inputs are not permitted400 (port from 9router#1468, #6420). Claude Code always sends a top-levelcontext_managementfield; strict anthropic-compatible gateways reject it. The dedicated context-editing 400-fallback only fired when OmniRoute's owncontextEditingfeature was enabled (default off), so a client-sent field passed through untouched and 400'd.context_managementis now in the generic reactive field-strip list, so it's stripped-and-retried once regardless of the feature flag (with correct request re-signing for claude-compatible relays). Regression guard:tests/unit/provider-field-strips.test.ts. (thanks @ohahe52-dot) -
fix(network): enable RFC 8305 Happy Eyeballs (
autoSelectFamily) on the direct-egress undici dispatcher (port from 9router#1237, #6423). When DNS returns both IPv6 (AAAA) and IPv4 (A) and the IPv6 route is broken (e.g. a NAT6464:ff9b::prefix without routing), undici tried IPv6 first and hung untilETIMEDOUT(then a 502 + account lockout), even thoughcurlreached the same host. The direct dispatcher now races both families and uses whichever connects first. Proxy paths pin family viaproxyTlsand are unaffected. Regression guard:tests/unit/direct-dispatcher-pipelining-4580.test.ts. (thanks @adentdk) -
fix(combo): round-robin now advances the rotation pointer past the model that actually served, not the eagerly-scheduled one (port from 9router#948, #6428). With
stickyLimit: 1(true round-robin), when the scheduled model failed and a different model served via fallback, the counter had already advanced +1 from the scheduled index — so the next request reused the fallback-served model, degrading round-robin into hot-spotting on whichever model was healthy. The pointer now advances to the served index + 1 (mirroring the sticky-limit>1 path). Session-stickiness (#3825) and distribution are preserved. Regression guard:tests/unit/combo-rr-fallback-advance-948.test.ts. (thanks @binsarjr) -
fix(sse): a non-string
modelfield is now rejected with a400before the resolver, instead of crashing downstream.toLowerCase()/.split()calls into an empty-body500that escapes the error sanitizer (#6407). Regression guard:tests/unit/chat-non-string-model-6407.test.ts. (thanks @chirag127) -
fix(api): unknown
/api/*routes now return a JSON404(instead of the dashboard HTML shell) and scalar chat params (model/temperature/etc.) are validated before the provider lookup so malformed requests fail fast with a clear400(#6424, #6412). Regression guards:tests/unit/api/api-catchall-json-404.test.ts,tests/unit/chat-early-schema-validation-6412.test.ts. (thanks @chirag127) -
fix(api):
/v1/chat/completionsnow rejects a non-JSONContent-Typewith a400before parsing the body (#6414). Regression guard:tests/unit/v1-chat-completions-content-type-6414.test.ts. (thanks @chirag127) -
fix(api): the
X-OmniRoute-Compressionresponse header is now echoed on/v1/chat/completionsand/v1/completions(#6422). Regression guard:tests/unit/compression-header-echo-6422.test.ts. (thanks @chirag127) -
fix(api): concurrent
GET /v1/modelsrequests are coalesced into a single catalog build (#6408). Regression guard:tests/unit/v1-models-concurrent-6408.test.ts. (thanks @chirag127) -
fix(api):
/v1/completionsnow echoes the requestedbody.modelin its JSON + streamed responses (#6429). Regression guard:tests/unit/completions-body-model-echo.test.ts. (thanks @chirag127) -
fix(api): env-var master keys now see the full
/v1/modelscatalog (#6406). Regression guard:tests/unit/models-catalog-envkey-6406.test.ts. (thanks @chirag127) -
fix(api): non-streaming
/v1/completionsresponses now echobody.modelaligned with theX-OmniRoute-Modelheader (#6426). Regression guard:tests/unit/v1-completions-model-header-match-6426.test.ts. (thanks @chirag127) -
fix(api): unknown
/v1/*routes now return a JSON404 not_foundinstead of the Next.js dashboard HTML shell (#6405). Regression guard:tests/unit/api/v1-catchall-json-404.test.ts. (thanks @chirag127) -
fix(api): the per-connection provider models route now degrades to the shipped catalog when a provider's
/modelsendpoint answers with a redirect (#6267) — aqwen-webimport failed with a rawRedirect blocked … (307)503.safeOutboundFetchthrowsREDIRECT_BLOCKEDon the 307,getSafeOutboundFetchErrorStatusmaps it to 503, andbuildDiscoveryErrorFallbackResponsetreated every 503 as a hard error — so the non-emptygetModelsByProviderId("qwen-web")catalog was never surfaced. A models-endpoint redirect is not a fixable-config error (unlikeURL_GUARD_BLOCKED/INVALID_URL, which stay hard errors), so it now falls back to the local/cached catalog before the 503 short-circuit. General fix — covers any config-driven provider that 307s. Regression guard:tests/unit/provider-models-qwen-web-redirect-6267.test.ts. (thanks @chirag127) -
fix(api): the per-connection provider models route (MCP
list_models_catalog+ the dashboard import view) now merges USER-ADDED custom models into its response (#6247) — custom models live in thekey_valuenamespacecustomModels, which the live REST/api/v1/modelsalready merges, butsrc/app/api/providers/[id]/models/route.tsnever readgetCustomModels, so custom models were dropped on both the discovery-success and local_catalog paths. They are now folded into the returned model list (deduped by id, stampedowned_by: provider), fixing MCP + the dashboard import view in one place. Regression guard:tests/unit/provider-models-custom-merge-6247.test.ts. (thanks @RCrushMe) -
fix(providers): GitLab Duo tool-calling follow-up turns no longer fail upstream with
422 {"detail":"Validation error"}(tokens 0/0, rejected pre-inference). The #6234 tool-result-feedback fix serialized the entire multi-turn conversation into GitLab's single-filecode_suggestions(small_file) generation endpoint — folded history that turn-N sent as an oversizedcurrent_file.content_above_cursorand duplicated verbatim intouser_instruction, tripping the AI-Gateway'ssmall_filevalidation guard. The executor now bounds that prompt: it keeps system + latest user message + the most-recent tool round (dropping older turns), caps oversized tool results, and stops duplicating the full prompt intouser_instruction(which now carries only the short latest user message) — while still feeding the most-recent tool result back so the agent continues (open-sse/executors/gitlab.ts, #6220). The unit test covers the bounding logic; the upstream 422→200 clearing is VPS-only (Hard Rule #18). Regression guard:tests/unit/gitlab-tool-exchange-bounded-6220.test.ts. -
fix(i18n): the provider-detail (
/dashboard/providers/[id]) connection-status filter labels no longer render as__MISSING__:All/__MISSING__:Active/__MISSING__:Error/__MISSING__:Banned/__MISSING__:CreditsExhaustedin non-English locales (notably pt-BR) (#6290). Root cause was not the namespace mismatch the issue guessed — theproviders.filter*keys resolve correctly inen.json; the debt lived in the locale mirrors (src/i18n/messages/*.json), where these five keys carried the__MISSING__:sync sentinel in ~15 locales and were absent entirely in ~26 others, so next-intl found the key and echoed the sentinel verbatim. All 40 non-English/-Chinese mirrors now ship real translations for the fiveproviders.filter*labels. Regression guard:tests/unit/i18n-provider-filter-keys-6290.test.ts. (thanks @diegosouzapw) -
fix(providers): the
copilot-m365-webstreaming executor now emitsdebug-level WebSocket diagnostics (#6210) — the outbound WS URL (with theaccess_tokenredacted viaredactWsUrl()), handshake success/failure, and each received SignalR frame'stype/target. Previously the streaming path logged nothing, so an emptycontent:nullresponse (the M365 Education / Starter tier symptom fixed in #6234) was undiagnosable even atAPP_LOG_LEVEL=debug. The change is debug-level and side-effect-free — it does not alter streaming behavior or the frame parser, and the token never reaches the logs. Regression guard:tests/unit/copilot-m365-web-logging-6210.test.ts(thanks @qpeyba) -
fix(resilience): a round-robin combo no longer returns
503 all upstream accounts are unavailablewhen a compatibility-rejected target is actually healthy (#6238).filterTargetsByRequestCompatibilitydrops request-incompatible targets (tool/vision/structured-output unsupported, or below the required context window) before any availability check runs, and itscompatible.length === 0safety net only fired when all targets were filtered — not when the kept targets later all turned out runtime-unavailable (circuit-open / cooldown / no credentials). So a combo could 503 while a compat-rejected-but-healthy provider sat unused.handleRoundRobinCombonow keeps the compat-rejected set and, when every compat-kept target was skipped without a single real attempt, probes those rejected targets as a last-resort fallback tier (via the new pureopen-sse/services/combo/comboCompatFallback.ts) before crystallizing the 503. Regression guard:tests/unit/combo-roundrobin-compat-fallback-6238.test.ts. (thanks @ThongAccount) -
fix(startup): best-effort self-heal for a corrupted Turbopack dev cache on Windows (#6289). On Windows,
pnpm devcan fail at startup when Turbopackmmaps a persistent-cache SST file and the OS refuses the mapping (os error 1455— "paging file too small"), which Turbopack surfaces as a misleadingModule not found: Can't resolve '@/shared/utils/machine'. This is a known upstream Turbopack cache-corruption bug — not our code. The dev launcher (scripts/dev/run-next.mjs) now wrapsnextApp.prepare()and, when it rejects with that signature (isTurbopackCacheCorruptionin the newscripts/dev/turbopackCacheHeal.mjs), purges.build/next/**/cache/turbopackand retries once with a clear log. Caveat — best-effort only: the corruption often surfaces as a runtime overlay rather than aprepare()rejection, so this cannot always intercept it; the reliable remedy remains manually deleting the Turbopack cache dir. Regression guard:tests/unit/turbopack-cache-heal-6289.test.ts. (thanks @chirag127) -
fix(providers): qodercli PAT auth no longer fails with
spawn qodercli ENOENTon Windows (#6263) —spawnQoderClispawned the bareqodercliname withshell:falseand an unenriched env, so the npm.cmdwrapper under%APPDATA%\npm(a user-PATH directory) was never resolved. It now resolves the absolute.cmd/.exepath through the existinggetCliRuntimeStatus("qoder")resolver insrc/shared/services/cliRuntime.ts(memoized), spawns withshellwhen the target is a.cmd/.bat, and uses the cliRuntime-enriched env (PATH + PATHEXT + APPDATA); the ENOENT error now lists the searched paths plus theCLI_QODER_BINoverride. End-to-end spawn on a real Windows host is host-only (Hard Rule #18); the path-resolution logic is unit-tested. Regression guard:tests/unit/qodercli-windows-resolve-6263.test.ts. (thanks @chirag127) -
fix(sse): the reasoning-token buffer no longer inflates probe-sized
max_tokens(#6274) — Claude Code's/modelcapability check sendsmax_tokens: 1, but for a thinking-capable model with a large output cap (e.g.glm-5.2) the #3587 headroom heuristic (max(current + 1000, ceil(current * 1.5))) rewrote it to1001and forwarded that upstream, wasting tokens on a request that was never a genuine reasoning budget.resolveReasoningBufferedMaxTokens()(open-sse/services/reasoningTokenBuffer.ts) now short-circuits and returns the caller's value verbatim when it is below the newREASONING_BUFFER_MIN_TRIGGER(256) threshold — a tiny explicit limit is a probe, not a reasoning request. Real budgets still receive the #3587 headroom unchanged, and the guard runs after the existing capability checks so unknown / non-reasoning models keep returningnull. Regression guard:tests/unit/reasoning-token-buffer-6274.test.ts. (thanks @brightfiscalband) -
fix(cli):
omniroute reset-passwordnow works as a real subcommand, and password resets over piped (non-TTY) stdin actually apply (#6261, #6258). Two coupled defects: (1) #6261 —bin/omniroute.mjsrouted everything through Commander with only two pre-Commander bypasses (--mcp,reset-encrypted-columns), soomniroute reset-passwordwas rejected as an unknown command; only the separateomniroute-reset-passwordbin worked, while the docs falsely advertised the subcommand (incl. a bogus "legacy alias still works"). A pre-Commander bypass mirroringreset-encrypted-columnsnow dynamically importsbin/reset-password.mjs(which self-executes) before Commander parses; the three doc lines were corrected. (2) #6258 —bin/reset-password.mjsissued two sequentialrl.questionprompts; under piped stdin the second read never settled at EOF, somain()never reachedresetManagementPasswordand the reset was a silent no-op (both prompts printed, no success, password unchanged). The CLI now detects non-TTY stdin and reads it once (first line = password, second line = confirm if present, else reused), adds a--password-stdinflag (entire stdin is the password, no confirmation), and exits0explicitly so the success line always flushes; interactive TTY behavior is unchanged. Regression guard:tests/unit/reset-password-cli-6261-6258.test.ts(3). (thanks @chirag127) -
fix(db): the mass-migration safety abort now tells the operator how to bypass it and stops flooding the log (#6260) — after restoring a backup that wiped the migration tracking table,
runMigrations()threw the abort on every downstreamensureDbInitialized(), re-logging the full banner 11+ times, and the message never mentioned the existingOMNIROUTE_MAX_PENDING_MIGRATIONSescape hatch. The abort text now appends a bypass hint (setOMNIROUTE_MAX_PENDING_MIGRATIONS=0inserver.env/DATA_DIR/.env), and a newMigrationSafetyAbortErroris memoized so repeated calls in the same process throw the same instance and emit a single concise line instead of the full cascade. Regression guard:tests/unit/migration-safety-abort-6260.test.ts. (thanks @chirag127) -
fix(auth): importing a distinct Codex/ChatGPT OAuth
auth.jsonis no longer falsely rejected as "already exists" when it belongs to a different user in the same workspace (#6301).findExistingCodexConnection(insrc/lib/oauth/utils/codexAuthImport.ts) deduped only onproviderSpecificData.workspaceId === accountId, whereaccountIdis the sharedchatgpt_account_id/tokens.account_id— so two members of the same ChatGPT Team collapsed onto a single connection (409duplicate_account). The id_token'shttps://api.openai.com/authclaim carries a per-userchatgpt_user_idalongside the workspace id (the device-flow path already persisted it aschatgptUserId, but the import path did not). NowparseAndValidateCodexAuthextractsuserId(chatgpt_user_id→user_id→ JWTsub) intoParsedCodexAuth, the create/update paths persistchatgptUserIdinproviderSpecificData(mirroringcodex.ts), and dedup keys onworkspaceIdANDchatgptUserId— with a backward-compat fallback to legacy accountId-only matching when no stored connection for that workspace records achatgptUserId, so genuinely-same accounts still dedup. Regression guard:tests/unit/codex-auth-import-userid-dedup-6301.test.ts(4). (thanks @anungma) -
fix(providers): importing models for the venice-web provider no longer fails with a red "Provider venice-web does not support models listing" (#6269).
venice-webis a web-cookie provider with an executor but no upstream/v1/modelsendpoint and no registrymodels, so the models route fell through to the tail400. Mirroring thejules/linkup-search/ollama-searchfix (#5569), it now ships a static local catalog entry insrc/lib/providers/staticModels.ts— seeding the current Venice lineup (venice-uncensored,llama-3.3-70b,qwen3-235b,qwen3-4b,deepseek-r1-671b; Venice rotates its catalog, see docs.venice.ai/models/overview) — so the route returns200withsource:"local_catalog",intentional:true. Regression guard:tests/unit/static-models-venice-web-6269.test.ts. (thanks @chirag127) -
fix(api): the specialty model catalogs (
/v1/embeddings,/v1/images,/v1/music,/v1/videosmodel lists) are now derived from the unified catalog filtered by a predicate (getSpecialtyModelsResponse) instead of ad-hoc per-route logic, so they consistently respect active-credential visibility and stay in sync with the main catalog (#6303). Regression guard:tests/unit/specialty-model-catalog-routes.test.ts. (thanks @makcimbx) -
fix(api): the agent-bridge server route now resolves the MITM manager via a dynamic
import("@/mitm/manager.runtime")so Turbopack does not statically pull the stub (or over-bundle the manager), and the agent-skills generator anchors its output base path withpath.join(process.cwd(), …)so Turbopack's static analyzer stops tracing the whole project root (#6329, #6366). Regression guard:tests/unit/agent-bridge-server-route-dynamic-import.test.ts. (thanks @Iammilansoni) -
fix(api): internal probes (combo-test, cloud-sync verify) now pick a management-scoped / allow-all API key instead of naively grabbing
getApiKeys()[0]— a restrictedself:usagefirst row made the probe fail with "Model X is not allowed for this API key" even when the combo path was healthy (pickApiKeyForInternalUseinsrc/lib/db/apiKeys.ts). The API-manager model editor also falls back to/api/models?all=truewhen/v1/modelsis catalog-protected (#6372). Regression guard:tests/unit/pick-internal-api-key-6372.test.ts. (thanks @jmengit) -
fix(live-ws): the Live Dashboard WebSocket server now rejects on bind failure (e.g.
EADDRINUSEwhen the API bridge already holds the port) instead of letting the error surface as an unhandlederrorevent that crash-loops the process — theerrorlistener is attached towss(notserver) and releases the EventBus subscription on a failed start (#6324). Regression guard:tests/unit/live-ws-eaddrinuse-6324.test.ts. (thanks @vinayakkulkarni) -
fix(dashboard): the Home provider-topology widget now trusts the live provider-metrics snapshot — it uses
topology.errorProviderand liveactiveRequestsdirectly instead of re-deriving state from a stalelastErrorAtor applying a frontend timeout filter, so the topology reflects real-time provider health (#6322). Regression guard:tests/unit/home-provider-topology-live-state.test.ts. (thanks @xz-dev) -
fix(sse): strip zero-width markers from streamed tool-call arguments — a follow-up to #5857. That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (
open-sse/services/claudeCodeObfuscation.ts) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Nowopen-sse/handlers/responseSanitizer.tsstrips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chattool_calls+ legacyfunction_call, native Responsesfunction_callitems, the OpenAI→Responses conversion, and the native Responses streamingresponse.function_call_arguments.delta/.doneevents). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases intests/unit/response-sanitizer.test.ts(suite 50/50). -
fix(nodejs): the default app log path now resolves under
DATA_DIR(~/.omniroute/logs/application/app.log) instead ofprocess.cwd()(#6197) — the globally-installed CLI runs from an arbitrary working directory, so anchoring the default to cwd made file logging silently write to (or no-op under) an unrelated directory, contradicting the documented.env.exampledefault.getAppLogFilePath()now computes the default lazily via the pureresolveDataDir()resolver (honours a per-processDATA_DIR, no directory-creation side effect); an explicitAPP_LOG_FILE_PATHstill wins. Regression guard:tests/unit/logenv-datadir-path-6197.test.ts(3). -
fix(docker): AgentBridge/
startMitmno longer aborts in containers/headless when the Antigravity-default DNS step can't write/etc/hosts(#6127), and the privileged command's stderr now reachesapp.loginstead of only a bare exit code hitting the toast (#6198). The default DNS step (addDNSEntry) was called unguarded while cert install and the two sibling DNS steps were each best-effort — in the runtime Docker image (USER node, nosudo, read-only/etc/hosts) it threwCommand failed with code 1out ofstartMitmInternaland killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effortprovisionDnsEntries()where each failure is logged with the fullerr(stderr included, folded in bysystemCommands.ts) and never aborts the start. Regression guard:tests/unit/mitm-dns-graceful-degrade-6127.test.ts(4). -
fix(providers): copilot-m365-web now supports the M365 Education "Starter / OfficeWebIncludedCopilot" tier and no longer returns an empty
content:nullstream (#6210). Two gaps: (1)buildWsUrl()hardcoded the individual-consumer scenario (OfficeWebPaidConsumerCopilot,isEdu=false) — the EDU tier is now opt-in viaproviderSpecificData.tier="edu", emittingscenario=OfficeWebIncludedCopilot/isEdu=true(the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas viaarguments[0].writeAtCursor(incremental) instead of onlymessages[].text(accumulated snapshots), which the parser dropped — a newaccumulateBotContent()folds both formats, withtype:2 item.result.messageas a last-resort fallback. Regression guard:tests/unit/copilot-m365-edu-writeatcursor-6210.test.ts(10). (thanks @qpeyba) -
fix(providers): GitLab Duo executor now feeds tool results back into the prompt instead of looping (#6220) —
buildPrompt()branched only onsystem/userand tookuserParts.at(-1), silently dropping theassistant{tool_calls}+tool{result}turns the client appended, so the reconstructed prompt was byte-identical to turn 1 and the model re-emitted the same<tool>call forever. When a tool exchange is present the full conversation is now serialized, folding each tool result back keyed by itstool_call_id; simple conversations keep the legacy shape. Complements the tool_call emission from #6051 (thekilo-duplicatelabel was a false positive — different, sequential defect). Regression guard:tests/unit/gitlab-tool-result-feedback-6220.test.ts(4). -
fix(providers): opencode-go/opencode-zen can now synthesize the OpenCode CLI identity headers Cloudflare requires on VPS egress (#5997) — on a datacenter VPS,
opencode.ai/zen/go/v1/chat/completions403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved thatUser-Agent: opencode-cli/1.0.0+x-opencode-client: cli+x-opencode-project: default+ fresh request/session UUIDs succeed. Opt-in viaOPENCODE_SYNTHESIZE_CLI_HEADERS=true(values overridable viaOPENCODE_GO_USER_AGENT/OPENCODE_USER_AGENT/OPENCODE_CLIENT/OPENCODE_PROJECT); it fills only headers the client did not already send. Kept off by default — the forward-only path is deliberate (fabricating a wrong value risks upstream rejection; a prior dedup regressed withopencode/local), so this replaces the fragile local header-injection shim without changing default behavior. Regression guard:tests/unit/opencode-cli-headers-synthesis-5997.test.ts(6). (thanks @aleksesipenko) -
fix(resilience): sticky session affinity now evicts and fails over to another account when the pinned account is exhausted/unavailable (#6219)
-
fix(sse): Responses API passthrough now drops internal commentary-phase output before forwarding to clients (gated by RESPONSES_PASSTHROUGH_DROP_COMMENTARY, default on) (#6199)
-
fix(sse): tool-call function schemas with a root
type: nullare now coerced totype: "object"before dispatch (#6359) — clients like the Codex app emitparameters: { type: null, ... }for some tools, which OpenAI-compatible upstreams reject with400 Invalid schema for function '...': schema must be a JSON Schema of 'type: "object"', got 'type: null', failing the whole request.toolSchemaSanitizeralready stripped the null; it now re-adds the mandatory root"object"type (and emptyproperties/openadditionalPropertieswhen absent). Combinator roots (anyOf/oneOf/allOf) and explicit root types are left untouched. Regression guard: 5 new cases intests/unit/tool-schema-sanitizer.test.mjs. -
fix(docker): AgentBridge no longer fails to start on npm/Electron/VPS installs with "MITM manager stub reached at runtime" (#6344) — v3.8.45 flipped the production bundler default to Turbopack, but
next.config.mjsaliased@/mitm/managerto its Docker-only degraded stub unconditionally. That was harmless while Docker (which sets the alias intentionally for #3390 graceful degradation) was the sole Turbopack consumer, but once every artifact built with Turbopack the stub shipped to all non-Docker users andstartMitmthrew on the first Agent-Bridge start. The alias is now opt-in viaOMNIROUTE_MITM_STUB=1(set only by the Dockerfile) through the sharedscripts/build/mitm-stub-flag.mjshelper; default builds bundle the real manager. Regression guard:tests/unit/mitm-stub-alias-6344.test.mjs(4). -
fix(proxy): stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies (#6246). Two coupled defects from the new health scheduler: (1) IP leak — when a proxy assigned to a connection was marked
inactive, resolution fell through to a direct egress instead of blocking, exposing the operator's real IP; (2) over-deactivation — the sweep flipped a proxy toinactiveon the first failed probe and counted our own 5s timeout / a probe-target5xxas the proxy's fault, so healthy paid proxies vanished from egress selection ("my proxies are not being used anymore"). Fix: the sweep decision is extracted into a pure, network-freedecideProxyHealthAction(src/lib/proxyHealth/decision.ts) — by default the health check now only counts/logs and never downgrades status (a proxy is downgraded/removed only withPROXY_AUTO_REMOVE=true, afterPROXY_AUTO_REMOVE_AFTERconsecutive conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a5xxfrom the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately,safeResolveProxynow fails closed via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (hasBlockingProxyAssignment), honoring the explicitproxy offtoggles and thePROXY_FAIL_OPEN=trueopt-out. Existing proxies stuckinactiveby the old behavior need a one-time manual re-activate (the operator owns proxy status). Regression guards:tests/unit/proxy-health-decide-action-6246.test.ts,tests/unit/proxy-assigned-unavailable-6246.test.ts. -
fix(proxy): make "Test All" read-only and add bulk enable/disable (#6246). Complements the core fail-closed / scheduler fix (#6296) with the two remaining reporter asks. (1) The "Test All" button (
POST /api/settings/proxies/auto-test) used to flip a proxy toinactiveon a failed reachability probe; since the egress selector excludesinactiveproxies, a flaky probe (an unreachablehttpbin.org, a proxy that blocksHEAD, or a slow paid proxy) silently disabled every proxy that failed — "Test All" is now read-only by default (only the operator sets a proxy active/inactive; opt back into the legacy test-and-set withPROXY_HEALTH_AUTO_DEACTIVATE=true). (2) Adds a bulk enable/disable proxies endpoint + toolbar action (POST /api/settings/proxies/batch-activate) so an operator can re-activate proxies in one click. Regression guard:tests/unit/proxy-health-6246.test.ts. (thanks @tenshiak) -
chatcore (tools): stop the default 128-tool cap from silently dropping opencode's
task/MCP tools. opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculativeMAX_TOOLS_LIMIT(128) default,truncateToolListdid a blindtools.slice(0, 128), dropping every tool past index 128 — including opencode's built-intasktool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream400s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (isOpencodeClient— anyx-opencode-*header, oropencodein the user-agent) now only bypasses the speculative 128 default, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. RefactorsgetEffectiveToolLimitintogetKnownToolLimit(provider) ?? DEFAULT_LIMIT(byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard:tests/unit/tool-limit-detector.test.ts. -
fix(mitm): the macOS MITM-cert install check now matches the system keychain again.
security find-certificate -a -Zprints the SHA-1 as a colon-less hex string, but the installed-check compared it againstgetCertFingerprint()'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extractedmacCertOutputHasFingerprinthelper. Regression guard:tests/unit/mitm-cert-mac-fingerprint.test.ts. (#6204, closes #6134 — thanks @rianonehub) -
fix(api):
/v1/messages/count_tokensnow countstool_use,tool_resultandthinkingcontent blocks (and array-formsystemprompts) in the local-estimation path, instead of onlytext. Real agentic conversations keep ~95% of their tokens inside tool results; the previous estimate returned near-zero for them, which silently broke Claude Code's auto-compaction (context grew past the window with no compaction until the upstream API rejected the request). The real provider-side count path is unchanged. Regression guard:tests/unit/messages-count-tokens-route.test.ts. (#6221 — thanks @luweiCN) -
fix(antigravity): strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s (#6114). Regression guard:
tests/unit/antigravity-claude-prefill-strip.test.ts. (thanks @anki1kr) -
fix(security): the mutable cloud-agent routes (
/api/cloud/credentials/update,/api/cloud/models/alias) now require management auth instead of being treated as public. They were classified as public API routes, so a request without management credentials could update stored cloud-agent credentials and model aliases. They are removed from the public-route set, classified as management routes in the authz pipeline, and gated byrequireManagementAuth; cloud read/auth routes stay public. Regression guards:tests/unit/cloud-write-auth.test.ts,tests/unit/authz/classify.test.ts,tests/unit/public-api-routes.test.ts. (#6233 — thanks @vittoroliveira-dev) -
refactor(dashboard): extract the onboarding-wizard "Open provider details" link target into a pure, unit-tested
buildProviderDetailsHref(connection)helper. The wizard already routes byconnection.id(the node UUID) rather than the provider category slug (#6144/#6145); this hardens that behavior behind a tested helper that guards a missing id/connection. Regression guard:tests/unit/provider-onboarding-href.test.ts. (#6166 — thanks @KooshaPari) -
fix(security): the doubao synthetic device-id generator now derives its digits via an unbiased crypto-random draw (rejection sampling over
crypto.randomBytes()) instead of a% 10reduction, closing a CodeQLjs/biased-cryptographic-randomfinding. -
fix(agentSkills): the GitHub-skills generator now resolves
outputDirto an absolute path before writing, fixing a regression introduced by #6366 (relative-to-cwd base path) that could write generated skill files to the wrong directory. -
fix(security):
/api/keys/{id}/devicesnow checks the HTTP method before auth/validation, returning a405for non-GET/DELETE verbs instead of a misleading401/500(closes adast-smokeQUERY-method finding). -
fix(quality): clear the last 2 heavy quality-gate reds on the release tip (cycle pre-flight).
-
fix(mitm): the test suite and CI can never mutate the OS trust store —
OMNIROUTE_SKIP_SYSTEM_TRUST=1is set globally for tests/CI soinstallCert/uninstallCert/installTproxyCaskip the privileged OS dispatch (#6310; full detail is under the [3.8.45] section below — this branch received it via the parallel-cycle sync-back). -
fix(api):
POST /api/github-skillsnow Zod-validates its request body; documented the new quality-gate env vars and pinned the merge-integrity GitHub Actions to a commit SHA. -
fix(skills): generate the missing
omni-github-skillsregistry entry and align the agent-skills catalog-count tests (follow-up to #6186). -
fix(quality): clear the cycle's 11 net-new ESLint errors and make
validate-release-greensuppressions-aware.
📝 Maintenance
- i18n(it): add 118 missing Italian (
it) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. (#6212 — thanks @serverless83) - chore(providers): remove deprecated MiMo V2 model entries from the catalogs (xiaomi-mimo, opencode-go, zenmux-free, audio TTS) — the upstream V2 line is superseded by MiMo V2.5; drops
mimo-v2-tts,mimo-v2-pro,mimo-v2-omni,mimo-v2-flash,mimo-v2-flash-freeand realigns the provider-catalog tests. (#6248 — thanks @backryun) - chore(release): ~50 commits on this branch are v3.8.45 pre-flight/hardening fixes and CI-perf work that landed here via the parallel-cycle sync-back (
sync-next-cycle.mjs, Hard Rule #21) after thev3.8.45git tag was cut, and are already fully documented under the [3.8.45] section below — listed here only so the per-cycle commit-coverage check (npm run release:uncovered) doesn't flag them as gaps. Provider/catalog/UX/backend fixes: #6041, #6078, #6108, #6135, #6148, #6149, #6154, #6158, #6161, #6162, #6163, #6164, #6165, #6170, #6177, #6178, #6181, #6186, #6187, #6191, #6193, #6194, #6195, #6200, #6205, #6208, #6209, #6211, #6213, #6223, #6224, #6225, #6226, #6227, #6228, #6229, #6230, #6235, #6291, #6292. CI/release-pipeline work: #6167, #6203, #6214, #6215, #6218, #6273, #6275, #6283, #6284, #6285, #6300, #6305. - chore(release): additional zero-ref release-cycle plumbing on this branch, kept out of
release:uncoveredon purpose (no#Nin the commit subject to cite): opening the v3.8.46 cycle, opening/closing the v3.8.45 cycle, the finalized [3.8.45] CHANGELOG i18n sync-back to 42 mirrors, the v3.8.45 cognitive/cyclomatic and file-size drift rebaselines, ESLint stale-suppression pruning (4,273 → 4,233), and clearing test-masking/docs-all pre-flight reds for v3.8.45.
⚡ Performance & Infrastructure
- perf(release-green): the pre-flight validator (
scripts/quality/validate-release-green.mjs) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) concurrently viaPromise.all— pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard:tests/unit/validate-release-green.test.ts("runs the slow suites CONCURRENTLY"). (#6319) - fix(ci):
scripts/release/sync-next-cycle.mjs— two defects found live in its first production run (v3.8.45 Phase 5): (1) thegit()helper's default 1 MiBmaxBuffercrashed withENOBUFSongit show origin/main:CHANGELOG.md(the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the[NEXT](TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs[prevVersion]bounded by the heading below it (new exported pure helperversionAfter). Guards: +5 tests intests/unit/sync-next-cycle.test.ts(8/8). (#6327) - test(ci): concurrency-sensitive flaky tests are quarantined into a serial pass (
tests/unit/serial/,--test-concurrency=1, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set:glm-coding-plan-monthly-3580,quota-division-blocks,provider-health-autopilot,combo-health-autopilot— the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in #6360. Guard:tests/unit/test-serial-quarantine.test.ts(4). (#6347)
🙌 Contributors
Thanks to everyone whose work landed in v3.8.46:
| Contributor | PRs / Issues |
|---|---|
| @2220258345 | direct commit / report |
| @abdofallah | direct commit / report |
| @adentdk | direct commit / report |
| @aleksesipenko | direct commit / report |
| @anki1kr | direct commit / report |
| @anungma | direct commit / report |
| @arpicato | direct commit / report |
| @backryun | #6248 |
| @binsarjr | direct commit / report |
| @brightfiscalband | direct commit / report |
| @chirag127 | #6501, #6506 |
| @developerjillur | direct commit / report |
| @dilneiss | #6499 |
| @dtybnrj | direct commit / report |
| @Forcerecon | direct commit / report |
| @hao3039032 | direct commit / report |
| @Iammilansoni | #6150, #6245 |
| @jmengit | direct commit / report |
| @jordansilly77-stack | direct commit / report |
| @JxnLexn | direct commit / report |
| @KooshaPari | #6166 |
| @loopyd | direct commit / report |
| @luweiCN | #6221 |
| @makcimbx | direct commit / report |
| @muflifadla38 | direct commit / report |
| @newnol | direct commit / report |
| @ofekbetzalel | direct commit / report |
| @ohahe52-dot | direct commit / report |
| @phidinhmanh | direct commit / report |
| @powellnorma | direct commit / report |
| @qpeyba | direct commit / report |
| @RaviTharuma | direct commit / report |
| @RCrushMe | direct commit / report |
| @rianonehub | #6134, #6204 |
| @serverless83 | #6212 |
| @swingtempo | direct commit / report |
| @tenshiak | direct commit / report |
| @ThongAccount | direct commit / report |
| @UnrealAryan | direct commit / report |
| @vinayakkulkarni | direct commit / report |
| @vittoroliveira-dev | #6233 |
| @warelik | direct commit / report |
| @xxy9468615 | direct commit / report |
| @xz-dev | direct commit / report |
| @yanpaing007 | direct commit / report |
| @diegosouzapw | maintainer |
What's Changed
- Release v3.8.46 by @diegosouzapw in #6314
Full Changelog: v3.8.45...v3.8.46