mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
docs(i18n): sync finalized [3.8.45] CHANGELOG section into 42 mirrors (parallel-cycle sync-back follow-up)
This commit is contained in:
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -63,7 +63,144 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.45] — TBD
|
||||
## [3.8.45] — 2026-07-06
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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`.
|
||||
- **feat(rankings):** add a **'Configured Only'** filter to the Free Provider Rankings page, so the table can be narrowed to just the providers you have configured connections for (with an empty-state hint when none are configured). New `en.json` keys and a pure filter helper covered by `tests/unit/free-provider-rankings-configured-filter.test.ts`. ([#6245](https://github.com/diegosouzapw/OmniRoute/pull/6245), closes [#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150) — thanks @Iammilansoni)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(mitm):** the test suite and CI can never mutate the OS trust store again — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` (set by the global test setup and all CI workflows) makes `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch while preserving the #4546 environment-skip contract. Root cause of the self-hosted runner incident: a cert-flow integration test installed a 105-byte fake PEM into `/usr/local/share/ca-certificates`, breaking ALL system TLS on the VM. Regression guard: `tests/unit/system-trust-test-guard.test.ts`. ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310))
|
||||
- **fix(security):** `/api/keys/{id}/devices` answers a clean method-first **405** for undocumented HTTP methods (e.g. the new `QUERY`) via a dedicated `http-method-guard` rule — the auth layer was answering 401 first, failing schemathesis's unsupported-methods check. Same pattern as the v3.8.44 TRACE fix. Regression guard: `tests/unit/dast-method-not-allowed.test.ts`.
|
||||
- **fix(combo):** the #6216 empty-stream failover is restricted to **truly empty bodies** (zero bytes — the Gemini HTTP-200-empty case), restoring the #3399/#3685 pass-through contracts for `[DONE]`-terminated empty streams and incomplete Claude lifecycles. New guard: `#5976 truly EMPTY streaming body → invalid for combo failover` (87/87 across both suites).
|
||||
- **fix(combo):** 5 streaming-path fixes — locked-stream 500, error-frame-only-if-no-content, Gemini `MALFORMED_RESPONSE`→content_filter failover, correlationId substring search, per-model-500 lockout skip + request-logger UI detail. Maintainer follow-up: `releaseQualityClone` cancels the abandoned quality-check tee branch (per-request memory) + regression test. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry (the #6186 catalog addition never ran the generator — 8 integration assertions split between old/new counts) and align the agent-skills catalog counts across integration + unit suites (43 = 23 API + 20 CLI; 44 with config).
|
||||
- **fix(a2a):** finish the #6186 catalog-count update — `listCapabilities` metadata reported `coverage.api.total: 22` (type literal + value) and `SkillCoverageSchema` pinned `z.literal(22)`, so the schema would REJECT the correct runtime value with 23 API skills. All three aligned to 23.
|
||||
- **fix(github-skills):** add a missing import, unit tests and a settings JSON-parse fix for the GitHub agent-skill discovery/import flow. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
|
||||
- **fix(api):** `POST /api/github-skills` validates its body with a Zod schema (`validateBody`) instead of blind `request.json()` destructuring — a non-array `targets` would crash `.map`. Regression guard: `tests/unit/github-skills-route-validation.test.ts`.
|
||||
- **fix(docker):** add `id=` to the BuildKit cache mounts so strict builders (e.g. buildkitd with strict frontend parsing) accept the Dockerfile. ([#6291](https://github.com/diegosouzapw/OmniRoute/pull/6291) — thanks @karimalsalah)
|
||||
- **fix(oauth):** register `zed` in the OAuth `PROVIDERS` map (fixes "Unknown provider" on the Zed sign-in flow) ([#6078](https://github.com/diegosouzapw/OmniRoute/pull/6078) — thanks @anki1kr), and align `zed` in `OAUTH_PROVIDER_IDS` + the config enum after the merge.
|
||||
- **fix(doubao-web):** switch the Doubao web provider to the Dola global endpoint. ([#6235](https://github.com/diegosouzapw/OmniRoute/pull/6235) — thanks @backryun)
|
||||
- **fix(doctor):** resolve two false-positive WARNs in the doctor diagnostics ([#6163](https://github.com/diegosouzapw/OmniRoute/pull/6163), closes [#6162](https://github.com/diegosouzapw/OmniRoute/issues/6162) — thanks @arssnndr)
|
||||
- **fix(providers):** refresh the GitHub Copilot model catalog to the current upstream set. ([#6154](https://github.com/diegosouzapw/OmniRoute/pull/6154) — thanks @backryun)
|
||||
- **fix(providers):** correct the Kiro model catalog to real upstream ids — fabricated `claude-opus-4.7`/`claude-sonnet-4.6` entries removed, real `claude-sonnet-5`/`claude-sonnet-4.5`/`claude-haiku-4.5` kept. ([#6170](https://github.com/diegosouzapw/OmniRoute/pull/6170))
|
||||
- **feat(sse):** surface Kiro adaptive-thinking reasoning frames as `reasoning_content` in the OpenAI-shaped stream. ([#6213](https://github.com/diegosouzapw/OmniRoute/pull/6213) — thanks @VXNCXNX)
|
||||
- **fix(cli):** use `OMNIROUTE_SERVER_HOST` instead of the POSIX auto-set `HOSTNAME` for the bind address (fixes wrong bind on POSIX shells that export HOSTNAME). ([#6195](https://github.com/diegosouzapw/OmniRoute/pull/6195), closes [#6194](https://github.com/diegosouzapw/OmniRoute/issues/6194) — thanks @Theadd)
|
||||
- **feat(provider):** add Claude 5 Sonnet to the Claude Web provider catalog. ([#6209](https://github.com/diegosouzapw/OmniRoute/pull/6209), closes [#6200](https://github.com/diegosouzapw/OmniRoute/issues/6200) — thanks @Iammilansoni)
|
||||
- **fix(providers):** add `nvidia` to `PROVIDER_TOOL_LIMITS` (1536) to prevent silent tool-list truncation. ([#6177](https://github.com/diegosouzapw/OmniRoute/pull/6177) — thanks @LuisAlejandroVega)
|
||||
- **fix(translator):** strip the `reasoning` param for nvidia `z-ai/glm-5.2` (upstream 400s on it). ([#6181](https://github.com/diegosouzapw/OmniRoute/pull/6181) — thanks @kanztu)
|
||||
- **fix(dashboard):** providers page gains a data-timeout guard and the live-WS standalone wiring (no more indefinite spinner when the data fetch stalls). ([#6211](https://github.com/diegosouzapw/OmniRoute/pull/6211))
|
||||
- **fix(sse):** surface the ChatGPT-web image silent-drop as an accurate error instead of an empty success. ([#6208](https://github.com/diegosouzapw/OmniRoute/pull/6208))
|
||||
- **fix(cline):** force upstream streaming for Cline/ClinePass (streaming-only API) — non-stream client requests are served from the buffered SSE. ([#6165](https://github.com/diegosouzapw/OmniRoute/pull/6165))
|
||||
- **fix(dashboard):** remove the always-on Auto-Routing (combo) banner from the home page — it did not reflect live routing state and reappeared on every fresh browser. Replacement guard: `tests/unit/home-no-autorouting-banner.test.ts`. ([#6164](https://github.com/diegosouzapw/OmniRoute/pull/6164))
|
||||
- **fix(dashboard):** stop a model-test error from freezing the page (React #31 object-as-child toast) — errors go through `extractApiErrorMessage`. ([#6161](https://github.com/diegosouzapw/OmniRoute/pull/6161))
|
||||
- **fix(oauth):** extract the keychain-import-only guard to its own module, restoring the oauth file-size freeze. ([#6158](https://github.com/diegosouzapw/OmniRoute/pull/6158))
|
||||
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/issues/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). (root cause independently diagnosed by @subhansh-dev in [#6298](https://github.com/diegosouzapw/OmniRoute/pull/6298) — thanks!)
|
||||
|
||||
- **fix(docker):** AgentBridge/`startMitm` no longer aborts in containers/headless when the Antigravity-default DNS step can't write `/etc/hosts` ([#6127](https://github.com/diegosouzapw/OmniRoute/issues/6127)), and the privileged command's stderr now reaches `app.log` instead of only a bare exit code hitting the toast ([#6198](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/issues/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 `400`s 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`. ([#6193](https://github.com/diegosouzapw/OmniRoute/pull/6193) — thanks @DKotsyuba)
|
||||
|
||||
- **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](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/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](https://github.com/diegosouzapw/OmniRoute/pull/6221) — thanks @luweiCN)
|
||||
|
||||
- **fix(antigravity):** strip a trailing assistant prefill turn for Vertex Claude models to avoid upstream 400s ([#6114](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6166) — thanks @KooshaPari)
|
||||
- **fix(api):** relay worker now binds the SSRF guard to a stable `const` name so minified standalone (Docker) builds resolve it ([#6149](https://github.com/diegosouzapw/OmniRoute/issues/6149)) — the Vercel/Deno relay generators embedded the shared `resolveRelayTarget` guard as a bare `${fn.toString()}` declaration while the worker body called the hardcoded literal name; SWC minification mangled the source function's name, so the deployed worker defined `<mangled>` but still called `resolveRelayTarget` → `ReferenceError`. Both templates now emit `const resolveRelayTarget = ${fn.toString()};` (the const name is a template literal, immune to minification). Regression guard: `tests/unit/relay-minified-fn-6149.test.ts` (4). (thanks @SeaXen)
|
||||
- **fix(providers):** refresh the stale NVIDIA NIM model registry — drop EOL `z-ai/glm-5.1`, add `z-ai/glm-5.2` and `nvidia/nemotron-3-ultra-550b-a55b` ([#6108](https://github.com/diegosouzapw/OmniRoute/issues/6108)). Regression guard: `tests/unit/nvidia-nim-registry-6108.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(backend):** GPT-family (codex) models now report a distinct `max_input_tokens` (272000) below their 400K `context_length` via an optional `maxInputTokens` on `RegistryModel`, so coding agents auto-compact correctly instead of overflowing the real input cap ([#6191](https://github.com/diegosouzapw/OmniRoute/issues/6191)). Regression guard: `tests/unit/gpt-max-input-tokens-6191.test.ts`. (thanks @luweiCN)
|
||||
- **fix(backend):** call logs now record a **reasoning source/char-count** (migration 116, `reasoning_source`/`reasoning_chars`) for models that emit `reasoning_content`/`<think>` but report zero reasoning tokens in usage, so `tokens_reasoning` no longer silently under-represents reasoning — cost math is unchanged (the priced `tokens_reasoning` stays usage-derived) ([#6187](https://github.com/diegosouzapw/OmniRoute/issues/6187)). Regression guard: `tests/unit/reasoning-token-source-6187.test.ts`. (thanks @andrea-kingautomation)
|
||||
- **fix(auth):** a stale/changed `STORAGE_ENCRYPTION_KEY` now surfaces as a clear **424 `storage_encryption_stale`** ("re-enter the API key") instead of a misleading "Auth failed: 401" — the connection's ciphertext failed to decrypt and was coerced to an empty Bearer, hiding the real cause ([#6148](https://github.com/diegosouzapw/OmniRoute/issues/6148)). Regression guard: `tests/unit/decrypt-stale-key-hint-6148.test.ts`. (thanks @chirag127)
|
||||
- **fix(backend):** memory injection now keeps the injected system message **first** for providers that require it (via a `PROVIDERS_SYSTEM_MUST_BE_FIRST` capability), instead of the cache-safe mid-array splice that made strict providers reject the request with a 400 ([#6135](https://github.com/diegosouzapw/OmniRoute/issues/6135)). Regression guard: `tests/unit/memory-system-first-6135.test.ts`.
|
||||
- **fix(services):** 9Router embed panel no longer 404s (optional catch-all route) and the supervisor probes the port before spawning to avoid raw EADDRINUSE ([#6205](https://github.com/diegosouzapw/OmniRoute/issues/6205)). Regression guards: `tests/unit/ninerouter-embed-port-6205.test.ts`, `tests/unit/services/ServiceSupervisor.test.ts`. (thanks @jonlwheat2-gif)
|
||||
- fix(mcp): forward the MCP request `extra` context through static tool loops so stdio callers keep their scope/identity ([#6178](https://github.com/diegosouzapw/OmniRoute/issues/6178))
|
||||
|
||||
### ⚡ Performance & Infrastructure
|
||||
|
||||
- **perf(test):** test-suite loader quick wins ([#6214](https://github.com/diegosouzapw/OmniRoute/pull/6214)) — the 19 test scripts switch `--import tsx` → `--import tsx/esm` (the repo is pure ESM; the unused CJS hook cost ~1.3s per test process × 2,462 processes — CI fast-path unit shards dropped 14.8→7.5 min, −49%), tsx bumped to ^4.23.0 (tsx#809 startup-regression fix), **37 orphan `.test.mjs` files (224 cases) recovered** into the canonical glob (they matched no runner and never ran in any CI job; `check:test-discovery` now scans `.mjs` too), and ci.yml/quality.yml unit jobs now call the canonical npm script `test:unit:ci:shard` (single source of truth — closes two silent drifts: missing `setupPolyfill` import in CI and `memory/`+`usage/` dirs absent from the fast-path glob). `tests/unit/dashboard/**` keeps the full tsx hook in its own invocation (`@lobehub/icons` es/ build internally `require()`s ESM-syntax files).
|
||||
- **ci:** heavy-pipeline dedup ([#6215](https://github.com/diegosouzapw/OmniRoute/pull/6215)) — the release-PR pipeline ran the unit suite 4× per sync (95 jobs, 208 machine-min; the v3.8.44 cycle fired 123 such runs, 88 cancelled). Now: Node 24/26 compat matrices move to a daily `nightly-compat.yml` (−28%/run; resolves the active release branch, opens a tracking issue on failure), coverage is collected inside the unit shards themselves via c8/`NODE_V8_COVERAGE` (−18%/run; the Coverage Shard ×8 matrix is gone — nodejs/node's own CI pattern), the ~40-job per-language i18n matrix becomes 1 job (the account has 20 concurrent-job slots total), and heavy jobs skip **draft** PRs — paired with `/generate-release` now opening the living release PR as draft (flipped ready at the new Phase 0a.0a), killing the per-merge churn for the whole cycle. Validated by a full `workflow_dispatch` of the new pipeline: 35 jobs, 0 failures, 23 min, merged coverage 80.16% (> ratchet baseline).
|
||||
- **feat(quality):** no-new-warnings per PR ([#6218](https://github.com/diegosouzapw/OmniRoute/pull/6218)) — native ESLint bulk suppressions (≥9.24) freeze the pre-existing debt (476 files / 4,273 violations in `config/quality/eslint-suppressions.json`); `npm run lint`, lint-staged (pre-commit) and a new fork-aware `lint-guard` job in quality.yml all run suppressions-aware, so a NEW warning goes red in the PR that introduces it instead of accruing invisibly (+41/+88 per cycle) and being blind-rebaselined at release. 3 warn rules promoted to error in `src/**` (`react-hooks/exhaustive-deps`, `@next/next/no-img-element`, `import/no-anonymous-default-export`); `collect-metrics` measures under the frozen baseline (ratchet metric = net-NEW debt; baseline tightened 4,279→0 in-PR per require-tighten); fork PRs run report-only (contributors are never blocked — the maintainer campaigns fix via co-authorship). Baseline stock shrinks via `--prune-suppressions` at release reconciliation.
|
||||
- **ci:** test jobs no longer wait on the Build gate ([#6275](https://github.com/diegosouzapw/OmniRoute/pull/6275)) — `test-unit`×8, `vitest`, `integration`×2 and `security` declared `needs: build` but never download the `next-build` artifact; they now start at minute 0 (`needs: changes`, same `if` as Build), cutting ~15–20 min of wall-clock per heavy run. `e2e`/`package-artifact`/`electron-smoke` keep `needs: build` (they consume the artifact for real).
|
||||
- **ci(build):** the ci.yml Build job compiles Next.js with **Turbopack** (`OMNIROUTE_USE_TURBOPACK=1`) ([#6273](https://github.com/diegosouzapw/OmniRoute/pull/6273)) — Build job 20 min → **6 min 59 s (~2.9×)** on ubuntu-latest; the webpack `actions/cache` step is removed. Validated end-to-end pre-merge via `gh workflow run ci.yml --ref <branch>`.
|
||||
- **feat(build):** **Turbopack becomes the default bundler** for `next build` and `next dev` ([#6283](https://github.com/diegosouzapw/OmniRoute/pull/6283)) — `build-next-isolated.mjs`, `run-next.mjs` and the playwright-runner default to Turbopack; `OMNIROUTE_USE_TURBOPACK=0` is the explicit webpack escape hatch. `nightly-compat.yml`/`npm-publish.yml` inherit the default. Regression guard: `tests/unit/build-bundler-default-turbopack.test.ts`.
|
||||
- **feat(docker):** the Docker image builds with Turbopack (`ENV OMNIROUTE_USE_TURBOPACK=1`) ([#6285](https://github.com/diegosouzapw/OmniRoute/pull/6285)) — the v3.8.27 ImportTracer panic ("unreachable: there must be a path to a root") does **not** reproduce on Next 16.2.9: amd64 (659 s) and arm64 (qemu) build clean, 0 panics, smoke health 200.
|
||||
- **ci:** opt-in **self-hosted VPS runners for the release window** ([#6284](https://github.com/diegosouzapw/OmniRoute/pull/6284)) — `scripts/vps/release-runner-up.sh`/`down.sh` manage the runner VM, and `build`/`test-unit`/`vitest` pick a dynamic `runs-on` gated by `vars.USE_VPS_RUNNER == 'true'` **and** own-origin (fork PRs never reach self-hosted runners). Wired into `/generate-release` (VM up at Phase 1, mandatory down at Phase 3).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **quality(release-green):** full pre-flight hardening for this release — the cycle's 11 net-new ESLint errors typed/fixed and `validate-release-green` made suppressions-aware with per-gate logs (`_artifacts/release-green/`) and a `--hermetic` mode; test-masking allowlist entries for the cycle's verified-legitimate assert reductions; stale ESLint suppressions pruned (4,273 → 4,233); the 7 net-new `as any` casts from #6292 typed; `githubSkillTools` MCP errors routed through `sanitizeErrorMessage()`; `combo-provider-cooldown-sibling` added to the Stryker tap set; executors/env docs count fixes.
|
||||
- **ci(quality):** merge-integrity fast-gates per PR — `check:changelog-integrity` (no base CHANGELOG bullet may vanish in the merge result — the auto-resolve "CHANGELOG-eat" pattern) and `check:agent-skills-sync` (generated SKILL.md ≡ catalog), blocking for own-origin branches and report-only for forks (Princípio Zero). ([#6300](https://github.com/diegosouzapw/OmniRoute/pull/6300))
|
||||
- **ci(vps):** hermetic `nightly-release-green` pre-flight on the dedicated `omni-release` self-hosted runner (dynamic `runs-on`, clean env); e2e/integration/electron stay on hosted runners (per-VM port collision + concurrent artifact-download limits documented in the PR). ([#6305](https://github.com/diegosouzapw/OmniRoute/pull/6305))
|
||||
- **chore(quality):** v3.8.45 cycle-close drift rebaselines — file-size (13 files grown by merged cycle PRs), cognitive 867→877, cyclomatic 2028→2035, kiro-translator test debt from #6213; all with dated justification keys.
|
||||
- **docs(architecture):** sync stale DB-layer counts (45+/55 → 95+/110+) in REPOSITORY_MAP, the db-schema diagram and llm.txt (+42 i18n mirrors). ([#6167](https://github.com/diegosouzapw/OmniRoute/pull/6167))
|
||||
- **chore(release):** parallel-cycle flow — `sync-next-cycle.mjs` + Hard Rule #21 semantics ([#6203](https://github.com/diegosouzapw/OmniRoute/pull/6203)); v3.8.45 development cycle opened.
|
||||
- **i18n(it):** add 118 missing Italian (`it`) translations (net-additive — no existing keys dropped, valid JSON), improving Italian UI coverage. ([#6212](https://github.com/diegosouzapw/OmniRoute/pull/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](https://github.com/diegosouzapw/OmniRoute/pull/6248) — thanks @backryun)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.45:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ----------------------------------- |
|
||||
| [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | direct commit / report |
|
||||
| [@anki1kr](https://github.com/anki1kr) | #6078 |
|
||||
| [@arssnndr](https://github.com/arssnndr) | #6162, #6163 |
|
||||
| [@backryun](https://github.com/backryun) | #6154, #6235, #6248 |
|
||||
| [@chirag127](https://github.com/chirag127) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #6193 |
|
||||
| [@hartmark](https://github.com/hartmark) | #6216 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6200, #6209, #6245 |
|
||||
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | direct commit / report |
|
||||
| [@kanztu](https://github.com/kanztu) | #6181 |
|
||||
| [@karimalsalah](https://github.com/karimalsalah) | #6291 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6166 |
|
||||
| [@LuisAlejandroVega](https://github.com/LuisAlejandroVega) | #6177 |
|
||||
| [@luweiCN](https://github.com/luweiCN) | #6221 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6186 |
|
||||
| [@muflifadla38](https://github.com/muflifadla38) | direct commit / report |
|
||||
| [@powellnorma](https://github.com/powellnorma) | direct commit / report |
|
||||
| [@qpeyba](https://github.com/qpeyba) | direct commit / report |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | direct commit / report |
|
||||
| [@rianonehub](https://github.com/rianonehub) | #6134, #6204 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@serverless83](https://github.com/serverless83) | #6212 |
|
||||
| [@subhansh-dev](https://github.com/subhansh-dev) | #6298 (diagnosis, landed via #6234) |
|
||||
| [@tenshiak](https://github.com/tenshiak) | direct commit / report |
|
||||
| [@Theadd](https://github.com/Theadd) | #6194, #6195 |
|
||||
| [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #6213 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user