github diegosouzapw/OmniRoute v3.8.46

4 hours ago

✨ New Features

  • feat(sse): hide paid-only models from auto/* routing when hidePaidModels is on (#6512) — follow-up to #6328/#6495. PR #6495 hid paid-only models from the GET /v1/models listing, but auto/* combos (auto/best-coding, auto/glm, …) could still pick a paid-only backend into their candidate pool → a 402/403 at request time. createVirtualAutoCombo now filters the candidate pool through the new pure open-sse/services/autoCombo/paidModelFilter.ts (filterPaidOnlyCandidates), applying the same free-model predicate #6495 uses in catalog.ts (providerHasFreeModels(provider) && isFreeModel(provider, {id})) whenever settings.hidePaidModels === true. Applied before the category/tier/family narrowing, so it covers every auto/* 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 combosauto/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 pure open-sse/services/autoCombo/modelFamily.ts (detectModelFamily) classifies by model-id prefix for six families; zai is instead resolved by provider id (z.ai's hosted API serves the same glm-* model ids as every other GLM backend, so auto/zai means "route to my z.ai backend specifically" vs auto/glm's "any connected GLM backend"). Reuses the existing createVirtualAutoCombo on-demand materialization path (no DB writes) and the /v1/models catalog 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.sql lifts the UNIQUE(scope, scope_id) constraint (rebuild via the canonical rename/copy/drop; existing single assignments become 1-element pools) and adds a proxy_scope_rotation companion table holding the per-scope strategy + a persisted monotonic round-robin cursor. Strategies: round-robin (default, monotonic cursor — never Math.random), random, and sticky-per-N-min. Resolution (resolveProxyForScopeFromRegistry / resolveProxyForConnectionFromRegistry) now fetches the alive, position-ordered candidate set (unchanged PROXY_ALIVE_PREDICATE) and applies the strategy; an empty / all-dead pool still returns null — 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 /v1beta endpoint (#6222) — both directions of the Gemini↔OpenAI conversion now preserve tool data (previously silently dropped). Request side: convertGeminiToInternal (extracted to its own testable module) maps tools[].functionDeclarations → OpenAI tools, prior functionCall parts → assistant tool_calls, and functionResponse parts → tool-role messages. Response side: convertOpenAIResponseToGemini emits parts[].functionCall {name,args} from message.tool_calls, and the streaming openAIChunkToGeminiChunk accumulates fragmented tool_calls deltas by index into complete functionCall parts. 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): M365ConnectionParams gains an agent field, a new opt-in M365_ENTERPRISE_OVERRIDES preset (agent=work, scenario=officeweb, licenseType=Premium) applies via providerSpecificData.tier="enterprise" (alias "work"), and agent is also overridable directly via providerSpecificData.agent. buildWsUrl was hardcoding agent="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 + thinking request params (#6241) — a thin standardization layer over the existing mature per-provider reasoning plumbing (no provider mapper touched). providerChatCompletionSchema gains a canonical effort (reusing the shared none/low/medium/high/xhigh vocabulary — the UI tiers extra/max collapse onto xhigh) and a boolean thinking. A pure normalizeReasoningRequest (wired once in src/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 client reasoning_effort / object-shaped thinking always wins (backward-compatible). /models additively exposes supportsThinking + effort_tiers so 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-step prompt (system instruction); only the final step's response is returned. Distinct from fusion (parallel fan-out + judge). Implemented as a self-contained open-sse/services/pipeline.ts (sibling to fusion.ts), dispatched from combo.ts; the step list reuses combo.models order and reads an optional prompt off 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's stream flag + 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-masking now 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>= 500 stayed 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-tested findReimplementedConditions() with an allowlist mirroring assertReductionAllowlist. 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 isCliproxyapiDeepModeEnabled helper is now wired into resolveExecutorWithProxy: a single connection can opt itself into the CLIProxyAPI passthrough executor via providerSpecificData.cliproxyapiMode="claude-native", with precedence connection override > provider upstream_proxy_config mode > default. resolveExecutorWithProxy now receives the resolved connection's providerSpecificData (threaded from chatCore.ts), so one connection can deep-route while the provider's other connections stay native — no DB schema change (the toggle rides in providerSpecificData). 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 -web cookie-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-tested resolveWebProviderHost() (prefers WEB_COOKIE_PROVIDERS[id].website, falls back to the registry baseUrl origin); 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 (base https://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 website via getProviderWebsiteHost), 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 extends tests/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-web was deferred (no verifiable web-session endpoint — needs a captured request) and huggingchat-web dropped (the existing huggingchat already 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_BUILTINS in open-sse/services/ccWireImageBuiltins.ts), consulted by isClaudeCodeCompatible / isClaudeCodeCompatibleProvider / applyFingerprint, makes agentrouter adopt the CC wire-image headers + fingerprint while guarding the CC baseUrl/auth branches so it keeps its own registry baseUrl and x-api-key auth. 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-ai is removed from the bulk-add exclusion list and the bulk parser gains a 3-field name|accountId|apiKey mode; the bulk route now builds a per-entry providerSpecificData so each key carries its own accountId (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 existing providerSpecificData.baseUrl persistence — 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 → global settings.disableSessionStickiness → default false (preserves the #3825 prompt-cache/504 fix); gates both stickiness call sites in open-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_SUDO env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (resolveSudoSpawn in src/mitm/systemCommands.ts) now strips the leading sudo when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without sudo (the operator trusts the CA manually, e.g. via NODE_EXTRA_CA_CERTS). Argv-array spawn preserved — 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 / ?availableOnly on GET /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, then main-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 validationErrors entry 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 useCodexResetCreditRedemption hook + /api/usage/codex-reset-credit route + codexResetCredits lib 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-cn connections (#6351) — a dedicated GlmTeamQuotaFields form section (team quota id / limits) threaded through the Add/Edit connection modals, persisted via providerSpecificData, 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-fetch executor + /v1/web/fetch route + 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-codex now spawns codex.cmd through a shell on Windows (the npm .cmd shim is unresolvable by bare spawn → 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 hidePaidModels setting that filters paid-only models out of the /v1/models catalog. 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) 403 responses (#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): sanitizeHeaders now redacts Set-Cookie response 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/preview now accepts mode: "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.5 and enx/codebuddy/gpt-5.5 both auto-generated the alias gpt-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 functionResponse co-located with other parts (another functionCall, or trailing text) in the same content when translating Gemini → OpenAI (#6376). convertGeminiContent() early-returned the tool message on the first functionResponse part, dropping any co-located parts; such contents are now pre-split (one tool message per functionResponse, 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 hardcoded PATH, but version managers expose their interpreters via shim dirs that only join PATH through 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, respecting MISE_DATA_DIR/PYENV_ROOT/ASDF_DATA_DIR when set), and a new HEADROOM_PYTHON env override lets operators point straight at their interpreter (mirroring HEADROOM_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_metadata passthrough field for NVIDIA requests (port from 9router#1887, #6411). NVIDIA's OpenAI-compatible wrapper rejects it with 400 Unsupported parameter, the same class already handled for cerebras/mistral; nvidia (executor default) 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 thinking field for NVIDIA z-ai/glm-5.2 (port from 9router#2023, #6413). NVIDIA's OpenAI-compatible wrapper 400s on thinking (a Claude-format client routed here leaves a thinking:{type:"adaptive"}); the existing strip rule only dropped reasoning. Same class already handled for minimax-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, and x-omniroute-thinking-marker: on force-restores it. Regression guard: tests/unit/think-close-marker-suppress-5245.test.ts. (thanks @abdofallah)

  • fix(executors): strip nested reasoning_content from messages for Mistral (port from 9router#1649, #6417). Mistral's API returns 422 extra_forbidden when an assistant message carries reasoning_content (replayed thinking from a prior turn, e.g. via the Codex /responses path); the generic top-level 400 field-downgrade retry never covered the nested per-message field. DefaultExecutor now strips it for provider mistral only, so DeepSeek (which requires replayed reasoning_content) is unaffected. Regression guard: tests/unit/mistral-strip-reasoning-content-1649.test.ts. (thanks @xxy9468615)

  • fix(executors): strip the client_metadata passthrough field on the OpenCode path (port from 9router#1442, #6418). OpenCode upstreams (e.g. kimi-k2.6 via opencode-go) reject it with 400 "Extra inputs are not permitted, field: 'client_metadata'"; the DefaultExecutor strip only covered cerebras/mistral and OpencodeExecutor extends BaseExecutor directly, so nothing removed it there. Regression guard: tests/unit/opencode-strip-client-metadata-1442.test.ts. (thanks @yanpaing007)

  • fix(executors): inject the reasoning_content echo for the native Moonshot Kimi provider (port from 9router#1480, #6419). Kimi (executor default) 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 to kimi (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 permitted 400 (port from 9router#1468, #6420). Claude Code always sends a top-level context_management field; strict anthropic-compatible gateways reject it. The dedicated context-editing 400-fallback only fired when OmniRoute's own contextEditing feature was enabled (default off), so a client-sent field passed through untouched and 400'd. context_management is 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 NAT64 64:ff9b:: prefix without routing), undici tried IPv6 first and hung until ETIMEDOUT (then a 502 + account lockout), even though curl reached the same host. The direct dispatcher now races both families and uses whichever connects first. Proxy paths pin family via proxyTls and 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 model field is now rejected with a 400 before the resolver, instead of crashing downstream .toLowerCase()/.split() calls into an empty-body 500 that 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 JSON 404 (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 clear 400 (#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/completions now rejects a non-JSON Content-Type with a 400 before parsing the body (#6414). Regression guard: tests/unit/v1-chat-completions-content-type-6414.test.ts. (thanks @chirag127)

  • fix(api): the X-OmniRoute-Compression response header is now echoed on /v1/chat/completions and /v1/completions (#6422). Regression guard: tests/unit/compression-header-echo-6422.test.ts. (thanks @chirag127)

  • fix(api): concurrent GET /v1/models requests are coalesced into a single catalog build (#6408). Regression guard: tests/unit/v1-models-concurrent-6408.test.ts. (thanks @chirag127)

  • fix(api): /v1/completions now echoes the requested body.model in 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/models catalog (#6406). Regression guard: tests/unit/models-catalog-envkey-6406.test.ts. (thanks @chirag127)

  • fix(api): non-streaming /v1/completions responses now echo body.model aligned with the X-OmniRoute-Model header (#6426). Regression guard: tests/unit/v1-completions-model-header-match-6426.test.ts. (thanks @chirag127)

  • fix(api): unknown /v1/* routes now return a JSON 404 not_found instead 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 /models endpoint answers with a redirect (#6267) — a qwen-web import failed with a raw Redirect blocked … (307) 503. safeOutboundFetch throws REDIRECT_BLOCKED on the 307, getSafeOutboundFetchErrorStatus maps it to 503, and buildDiscoveryErrorFallbackResponse treated every 503 as a hard error — so the non-empty getModelsByProviderId("qwen-web") catalog was never surfaced. A models-endpoint redirect is not a fixable-config error (unlike URL_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 the key_value namespace customModels, which the live REST /api/v1/models already merges, but src/app/api/providers/[id]/models/route.ts never read getCustomModels, 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, stamped owned_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-file code_suggestions (small_file) generation endpoint — folded history that turn-N sent as an oversized current_file.content_above_cursor and duplicated verbatim into user_instruction, tripping the AI-Gateway's small_file validation 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 into user_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__:CreditsExhausted in non-English locales (notably pt-BR) (#6290). Root cause was not the namespace mismatch the issue guessed — the providers.filter* keys resolve correctly in en.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 five providers.filter* labels. Regression guard: tests/unit/i18n-provider-filter-keys-6290.test.ts. (thanks @diegosouzapw)

  • fix(providers): the copilot-m365-web streaming executor now emits debug-level WebSocket diagnostics (#6210) — the outbound WS URL (with the access_token redacted via redactWsUrl()), handshake success/failure, and each received SignalR frame's type/target. Previously the streaming path logged nothing, so an empty content:null response (the M365 Education / Starter tier symptom fixed in #6234) was undiagnosable even at APP_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 unavailable when a compatibility-rejected target is actually healthy (#6238). filterTargetsByRequestCompatibility drops request-incompatible targets (tool/vision/structured-output unsupported, or below the required context window) before any availability check runs, and its compatible.length === 0 safety 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. handleRoundRobinCombo now 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 pure open-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 dev can fail at startup when Turbopack mmaps a persistent-cache SST file and the OS refuses the mapping (os error 1455 — "paging file too small"), which Turbopack surfaces as a misleading Module 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 wraps nextApp.prepare() and, when it rejects with that signature (isTurbopackCacheCorruption in the new scripts/dev/turbopackCacheHeal.mjs), purges .build/next/**/cache/turbopack and retries once with a clear log. Caveat — best-effort only: the corruption often surfaces as a runtime overlay rather than a prepare() 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 ENOENT on Windows (#6263) — spawnQoderCli spawned the bare qodercli name with shell:false and an unenriched env, so the npm .cmd wrapper under %APPDATA%\npm (a user-PATH directory) was never resolved. It now resolves the absolute .cmd/.exe path through the existing getCliRuntimeStatus("qoder") resolver in src/shared/services/cliRuntime.ts (memoized), spawns with shell when the target is a .cmd/.bat, and uses the cliRuntime-enriched env (PATH + PATHEXT + APPDATA); the ENOENT error now lists the searched paths plus the CLI_QODER_BIN override. 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 /model capability check sends max_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 to 1001 and 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 new REASONING_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 returning null. Regression guard: tests/unit/reasoning-token-buffer-6274.test.ts. (thanks @brightfiscalband)

  • fix(cli): omniroute reset-password now works as a real subcommand, and password resets over piped (non-TTY) stdin actually apply (#6261, #6258). Two coupled defects: (1) #6261bin/omniroute.mjs routed everything through Commander with only two pre-Commander bypasses (--mcp, reset-encrypted-columns), so omniroute reset-password was rejected as an unknown command; only the separate omniroute-reset-password bin worked, while the docs falsely advertised the subcommand (incl. a bogus "legacy alias still works"). A pre-Commander bypass mirroring reset-encrypted-columns now dynamically imports bin/reset-password.mjs (which self-executes) before Commander parses; the three doc lines were corrected. (2) #6258bin/reset-password.mjs issued two sequential rl.question prompts; under piped stdin the second read never settled at EOF, so main() never reached resetManagementPassword and 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-stdin flag (entire stdin is the password, no confirmation), and exits 0 explicitly 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 downstream ensureDbInitialized(), re-logging the full banner 11+ times, and the message never mentioned the existing OMNIROUTE_MAX_PENDING_MIGRATIONS escape hatch. The abort text now appends a bypass hint (set OMNIROUTE_MAX_PENDING_MIGRATIONS=0 in server.env / DATA_DIR/.env), and a new MigrationSafetyAbortError is 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.json is no longer falsely rejected as "already exists" when it belongs to a different user in the same workspace (#6301). findExistingCodexConnection (in src/lib/oauth/utils/codexAuthImport.ts) deduped only on providerSpecificData.workspaceId === accountId, where accountId is the shared chatgpt_account_id/tokens.account_id — so two members of the same ChatGPT Team collapsed onto a single connection (409 duplicate_account). The id_token's https://api.openai.com/auth claim carries a per-user chatgpt_user_id alongside the workspace id (the device-flow path already persisted it as chatgptUserId, but the import path did not). Now parseAndValidateCodexAuth extracts userId (chatgpt_user_iduser_id → JWT sub) into ParsedCodexAuth, the create/update paths persist chatgptUserId in providerSpecificData (mirroring codex.ts), and dedup keys on workspaceId AND chatgptUserId — with a backward-compat fallback to legacy accountId-only matching when no stored connection for that workspace records a chatgptUserId, 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-web is a web-cookie provider with an executor but no upstream /v1/models endpoint and no registry models, so the models route fell through to the tail 400. Mirroring the jules/linkup-search/ollama-search fix (#5569), it now ships a static local catalog entry in src/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 returns 200 with source:"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/videos model 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 with path.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 restricted self:usage first row made the probe fail with "Model X is not allowed for this API key" even when the combo path was healthy (pickApiKeyForInternalUse in src/lib/db/apiKeys.ts). The API-manager model editor also falls back to /api/models?all=true when /v1/models is 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. EADDRINUSE when the API bridge already holds the port) instead of letting the error surface as an unhandled error event that crash-loops the process — the error listener is attached to wss (not server) 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.errorProvider and live activeRequests directly instead of re-deriving state from a stale lastErrorAt or 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). Now open-sse/handlers/responseSanitizer.ts strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat tool_calls + legacy function_call, native Responses function_call items, the OpenAI→Responses conversion, and the native Responses streaming response.function_call_arguments.delta/.done events). 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 in tests/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 of process.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.example default. getAppLogFilePath() now computes the default lazily via the pure resolveDataDir() resolver (honours a per-process DATA_DIR, no directory-creation side effect); an explicit APP_LOG_FILE_PATH still wins. Regression guard: tests/unit/logenv-datadir-path-6197.test.ts (3).

  • fix(docker): AgentBridge/startMitm no longer aborts in containers/headless when the Antigravity-default DNS step can't write /etc/hosts (#6127), and the privileged command's stderr now reaches app.log instead 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, no sudo, read-only /etc/hosts) it threw Command failed with code 1 out of startMitmInternal and killed the whole start, discarding the stderr. The three DNS steps are extracted into a best-effort provisionDnsEntries() where each failure is logged with the full err (stderr included, folded in by systemCommands.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:null stream (#6210). Two gaps: (1) buildWsUrl() hardcoded the individual-consumer scenario (OfficeWebPaidConsumerCopilot, isEdu=false) — the EDU tier is now opt-in via providerSpecificData.tier="edu", emitting scenario=OfficeWebIncludedCopilot/isEdu=true (the individual path is unchanged); (2) the EDU/GPT-5.5 path streams deltas via arguments[0].writeAtCursor (incremental) instead of only messages[].text (accumulated snapshots), which the parser dropped — a new accumulateBotContent() folds both formats, with type:2 item.result.message as 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 on system/user and took userParts.at(-1), silently dropping the assistant{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 its tool_call_id; simple conversations keep the legacy shape. Complements the tool_call emission from #6051 (the kilo-duplicate label 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/completions 403s (HTML challenge) requests lacking CLI identity, while the reporter's control curl proved that User-Agent: opencode-cli/1.0.0 + x-opencode-client: cli + x-opencode-project: default + fresh request/session UUIDs succeed. Opt-in via OPENCODE_SYNTHESIZE_CLI_HEADERS=true (values overridable via OPENCODE_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 with opencode/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: null are now coerced to type: "object" before dispatch (#6359) — clients like the Codex app emit parameters: { type: null, ... } for some tools, which OpenAI-compatible upstreams reject with 400 Invalid schema for function '...': schema must be a JSON Schema of 'type: "object"', got 'type: null', failing the whole request. toolSchemaSanitizer already stripped the null; it now re-adds the mandatory root "object" type (and empty properties/open additionalProperties when absent). Combinator roots (anyOf/oneOf/allOf) and explicit root types are left untouched. Regression guard: 5 new cases in tests/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.mjs aliased @/mitm/manager to 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 and startMitm threw on the first Agent-Bridge start. The alias is now opt-in via OMNIROUTE_MITM_STUB=1 (set only by the Dockerfile) through the shared scripts/build/mitm-stub-flag.mjs helper; 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 to inactive on the first failed probe and counted our own 5s timeout / a probe-target 5xx as 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-free decideProxyHealthAction (src/lib/proxyHealth/decision.ts) — by default the health check now only counts/logs and never downgrades status (a proxy is downgraded/removed only with PROXY_AUTO_REMOVE=true, after PROXY_AUTO_REMOVE_AFTER consecutive conclusive failures); probes are classified tri-state so an inconclusive result (our timeout, or a 5xx from the probe target) never penalizes the proxy, and the probe timeout is raised 5s→15s. Separately, safeResolveProxy now fails closed via the existing policy: a connection whose assigned proxy is dead is blocked instead of leaking direct (hasBlockingProxyAssignment), honoring the explicit proxy off toggles and the PROXY_FAIL_OPEN=true opt-out. Existing proxies stuck inactive by 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 to inactive on a failed reachability probe; since the egress selector excludes inactive proxies, a flaky probe (an unreachable httpbin.org, a proxy that blocks HEAD, 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 with PROXY_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 speculative MAX_TOOLS_LIMIT (128) default, truncateToolList did a blind tools.slice(0, 128), dropping every tool past index 128 — including opencode's built-in task tool (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 upstream 400s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (isOpencodeClient — any x-opencode-* header, or opencode in 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. Refactors getEffectiveToolLimit into getKnownToolLimit(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 -Z prints the SHA-1 as a colon-less hex string, but the installed-check compared it against getCertFingerprint()'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 extracted macCertOutputHasFingerprint helper. Regression guard: tests/unit/mitm-cert-mac-fingerprint.test.ts. (#6204, closes #6134 — thanks @rianonehub)

  • fix(api): /v1/messages/count_tokens now counts tool_use, tool_result and thinking content blocks (and array-form system prompts) in the local-estimation path, instead of only text. 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 by requireManagementAuth; 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 by connection.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 % 10 reduction, closing a CodeQL js/biased-cryptographic-random finding.

  • fix(agentSkills): the GitHub-skills generator now resolves outputDir to 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}/devices now checks the HTTP method before auth/validation, returning a 405 for non-GET/DELETE verbs instead of a misleading 401/500 (closes a dast-smoke QUERY-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=1 is set globally for tests/CI so installCert/uninstallCert/installTproxyCa skip 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-skills now 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-skills registry 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-green suppressions-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-free and 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 the v3.8.45 git 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:uncovered on purpose (no #N in 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 via Promise.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) the git() helper's default 1 MiB maxBuffer crashed with ENOBUFS on git 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 helper versionAfter). Guards: +5 tests in tests/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

Full Changelog: v3.8.45...v3.8.46

Don't miss a new OmniRoute release

NewReleases is sending notifications on new releases.