# Changelog (中文 (简体)) 🌐 **Languages:** 🇺🇸 [English](../../../CHANGELOG.md) · 🇸🇦 [ar](../ar/CHANGELOG.md) · 🇧🇬 [bg](../bg/CHANGELOG.md) · 🇧🇩 [bn](../bn/CHANGELOG.md) · 🇨🇿 [cs](../cs/CHANGELOG.md) · 🇩🇰 [da](../da/CHANGELOG.md) · 🇩🇪 [de](../de/CHANGELOG.md) · 🇪🇸 [es](../es/CHANGELOG.md) · 🇮🇷 [fa](../fa/CHANGELOG.md) · 🇫🇮 [fi](../fi/CHANGELOG.md) · 🇫🇷 [fr](../fr/CHANGELOG.md) · 🇮🇳 [gu](../gu/CHANGELOG.md) · 🇮🇱 [he](../he/CHANGELOG.md) · 🇮🇳 [hi](../hi/CHANGELOG.md) · 🇭🇺 [hu](../hu/CHANGELOG.md) · 🇮🇩 [id](../id/CHANGELOG.md) · 🇮🇹 [it](../it/CHANGELOG.md) · 🇯🇵 [ja](../ja/CHANGELOG.md) · 🇰🇷 [ko](../ko/CHANGELOG.md) · 🇮🇳 [mr](../mr/CHANGELOG.md) · 🇲🇾 [ms](../ms/CHANGELOG.md) · 🇳🇱 [nl](../nl/CHANGELOG.md) · 🇳🇴 [no](../no/CHANGELOG.md) · 🇵🇭 [phi](../phi/CHANGELOG.md) · 🇵🇱 [pl](../pl/CHANGELOG.md) · 🇵🇹 [pt](../pt/CHANGELOG.md) · 🇧🇷 [pt-BR](../pt-BR/CHANGELOG.md) · 🇷🇴 [ro](../ro/CHANGELOG.md) · 🇷🇺 [ru](../ru/CHANGELOG.md) · 🇸🇰 [sk](../sk/CHANGELOG.md) · 🇸🇪 [sv](../sv/CHANGELOG.md) · 🇰🇪 [sw](../sw/CHANGELOG.md) · 🇮🇳 [ta](../ta/CHANGELOG.md) · 🇮🇳 [te](../te/CHANGELOG.md) · 🇹🇭 [th](../th/CHANGELOG.md) · 🇹🇷 [tr](../tr/CHANGELOG.md) · 🇺🇦 [uk-UA](../uk-UA/CHANGELOG.md) · 🇵🇰 [ur](../ur/CHANGELOG.md) · 🇻🇳 [vi](../vi/CHANGELOG.md) · 🇨🇳 [zh-CN](../zh-CN/CHANGELOG.md) --- ## [3.8.31] — 2026-06-20 ## [3.8.48] — 2026-07-13 > ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here. ### 🐛 Bug Fixes - **fix(build):** ship `dist/head-response-guard.cjs` in the npm tarball — the prepublish prune allowlist lacked it, so every `omniroute` boot of the published 3.8.47 crashed with `ERR_MODULE_NOT_FOUND` (3rd occurrence of this class after tls-options/3.8.41); now allowlisted, enforced by `check:pack-artifact`, and guarded by a closure test that derives every `server-ws.mjs` sibling import ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065), [#7040](https://github.com/diegosouzapw/OmniRoute/issues/7040)) - **fix(build):** Electron Windows packaging — the better-sqlite3 Electron-ABI rebuild now spawns `npx.cmd` through a shell (Node's CVE-2024-27980 hardening made the shell-less spawn fail with `status null` on Windows runners, breaking the v3.8.47 desktop build) - **fix(ci):** Sonar quality gate zeroed on new code — the coverage lcov now reaches the scanner at `coverage/lcov.info` (it read 0% on every scan), the async `isCloudEnabled()` gate in the Kiro auto-import route is awaited (cloud sync ran even when disabled), the dead `structuredClone` fallback in the reasoning-split clone is a real JSON fallback, the codex executor handles the async `reader.cancel()` rejection, deterministic `localeCompare` sorts, a path-traversal guard in `classify-pr-changes.mjs`, and the Docker better-sqlite3 rebuild uses npm's bundled node-gyp instead of `npx --yes` - **chore(ci):** the Sonar quality gate is informational (`sonar.qualitygate.wait=false`) while the org's SonarCloud plan cannot associate the tuned "OmniRoute way" gate (coverage ≥60 aligned with the repo floor) --- ## [3.8.47] — 2026-07-13 _Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._ - **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten). ### ✨ New Features - **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127) - **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77) - **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77) - **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems) - **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab) - **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok) - **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen) - **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw) - **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw) - **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw) - **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw) - **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber) - **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470) - **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625) - **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev) - **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen) - **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7) - **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333) - **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `/` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok) - **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc) - **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp) - **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333) - **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7) - **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`. - **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna) - **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`. - **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi). - **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky). - **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661). - **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari). - **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211) - **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit) - **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev). - **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc). - **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell). - **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn). - **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell). - **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/-.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`. - **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan). - **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127). - **feat(dashboard):** search box on the Playground's raw model `` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127) - **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`. - **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur) - **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog) - **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer) - **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur) - **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. - **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`. - **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur) - **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`. - **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`. - **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`. - **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`. - **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`. - **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`. - **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`. - **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`. - **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting). - **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request"). - **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz) - **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot) - **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77) - **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`. - **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild ` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br) - **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen) - **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape) - **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn) - fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142) - fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272) - fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276) - **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun - **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2). - **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev). - **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev). - fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343) - fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) - fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799) - fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409) - fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491) - fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524) - **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky). - **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)). - **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev). - fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) - fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628) - fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802) - **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari). - fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) - fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637) - **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub). - **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun). - **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698). - **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis) - **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin) - **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit) - **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820) - **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk) - **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev - **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`. - **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584) - **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills//SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`). - **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819). - **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`. - **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage). - **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact). - **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`. - **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab) - **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant) - **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text. - **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. - **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). - **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. - **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). - **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). - **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756). - **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos). - fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766)) - **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127). - fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772) - fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773) - **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo - **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787). - **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753). - **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch). - **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch). - **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell). - fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800) - fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803) - **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong). - fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) - **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi) - **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`. - **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos). - **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). - **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). - fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854) - **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari) - **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859). - fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876) - fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906) - **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908) - fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911) - fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912) - fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914) - fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925) - fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927) - perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930) - fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932) - fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935) - fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936) - fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939) - fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu) - **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl - fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947) - fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951) - fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952) - **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975). - **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957). ### 📝 Maintenance - **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw) - **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw) - **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw) - **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari) - **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari) - **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases). - **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11. - **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. - **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878)) - chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift) - chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract - chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up) - **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693). - **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663). - chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169 - **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring. - **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump). - **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc. - **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`. ### 🙌 Contributors Thanks to everyone whose work landed in v3.8.47: | Contributor | PRs / Issues | | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 | | [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 | | [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 | | [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 | | [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report | | [@artickc](https://github.com/artickc) | #6363, #6763 | | [@backryun](https://github.com/backryun) | #6280, #6675, #6862 | | [@blackwell-systems](https://github.com/blackwell-systems) | #6838 | | [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 | | [@charleszolot](https://github.com/charleszolot) | #6571 | | [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 | | [@chy1211](https://github.com/chy1211) | direct commit / report | | [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 | | [@deadcoder0904](https://github.com/deadcoder0904) | #6665 | | [@developerjillur](https://github.com/developerjillur) | direct commit / report | | [@eidoog](https://github.com/eidoog) | direct commit / report | | [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 | | [@gdevenyi](https://github.com/gdevenyi) | direct commit / report | | [@growab](https://github.com/growab) | #6867 | | [@hajilok](https://github.com/hajilok) | #6126, #6833 | | [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 | | [@herjarsa](https://github.com/herjarsa) | direct commit / report | | [@iamraydoan](https://github.com/iamraydoan) | #6798 | | [@ianriizky](https://github.com/ianriizky) | #6072, #6538 | | [@itiwant](https://github.com/itiwant) | direct commit / report | | [@janeza2](https://github.com/janeza2) | #6308 | | [@JxnLexn](https://github.com/JxnLexn) | #6776 | | [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 | | [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report | | [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report | | [@lunkerchen](https://github.com/lunkerchen) | #6320 | | [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 | | [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 | | [@nowhats-br](https://github.com/nowhats-br) | direct commit / report | | [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 | | [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 | | [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 | | [@quanturbo](https://github.com/quanturbo) | #6780 | | [@rafaumeu](https://github.com/rafaumeu) | #6813 | | [@rafpigna](https://github.com/rafpigna) | #6574 | | [@rucciva](https://github.com/rucciva) | #4125 | | [@rushsinging](https://github.com/rushsinging) | #6807 | | [@ryanngit](https://github.com/ryanngit) | direct commit / report | | [@samir-abis](https://github.com/samir-abis) | direct commit / report | | [@SeaXen](https://github.com/SeaXen) | direct commit / report | | [@shabeer](https://github.com/shabeer) | direct commit / report | | [@Squawk7777](https://github.com/Squawk7777) | #6565 | | [@strangersp](https://github.com/strangersp) | #6587 | | [@Thinkscape](https://github.com/Thinkscape) | direct commit / report | | [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 | | [@tjengbudi](https://github.com/tjengbudi) | #4009 | | [@whale9820](https://github.com/whale9820) | direct commit / report | | [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 | | [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 | | [@yinaoxiong](https://github.com/yinaoxiong) | #6805 | | [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- ## [3.8.46] — 2026-07-07 ### ✨ New Features - **feat(sse):** **hide paid-only models from `auto/*` routing** when `hidePaidModels` is on ([#6512](https://github.com/diegosouzapw/OmniRoute/issues/6512)) — follow-up to #6328/#6495. PR #6495 hid paid-only models from the `GET /v1/models` listing, but `auto/*` combos (`auto/best-coding`, `auto/glm`, …) could still pick a paid-only backend into their candidate pool → a 402/403 at request time. `createVirtualAutoCombo` now filters the candidate pool through the new pure `open-sse/services/autoCombo/paidModelFilter.ts` (`filterPaidOnlyCandidates`), applying the same free-model predicate #6495 uses in `catalog.ts` (`providerHasFreeModels(provider) && isFreeModel(provider, {id})`) whenever `settings.hidePaidModels === true`. Applied before the category/tier/family narrowing, so it covers every `auto/*` combo; an all-paid pool degrades to the existing graceful empty-pool path. **Opt-in — default OFF leaves the pool unchanged** (identity). Regression guard: `tests/unit/autoCombo/paid-model-filter-6512.test.ts` (4, incl. the default-off identity guard). - **feat(sse):** **provider-family auto combos** — `auto/glm`, `auto/minimax`, `auto/mimo`, `auto/zai`, `auto/gemma`, `auto/llama`, `auto/gemini` ([#6453](https://github.com/diegosouzapw/OmniRoute/issues/6453)) — new routable ids that materialize an on-demand virtual combo spanning whatever installed backends currently expose that model family, degrading gracefully as backends rotate. A new pure `open-sse/services/autoCombo/modelFamily.ts` (`detectModelFamily`) classifies by model-id prefix for six families; `zai` is instead resolved by provider id (z.ai's hosted API serves the same `glm-*` model ids as every other GLM backend, so `auto/zai` means "route to my z.ai backend specifically" vs `auto/glm`'s "any connected GLM backend"). Reuses the existing `createVirtualAutoCombo` on-demand materialization path (no DB writes) and the `/v1/models` catalog advertising loop. Regression guard: `tests/unit/autoCombo/provider-family-combos.test.ts` (11). - **feat(proxy):** native **proxy-pool round-robin / egress IP rotation** ([#6365](https://github.com/diegosouzapw/OmniRoute/issues/6365)) — a scope (global / provider / account) can now hold **multiple** proxies as a pool with a rotation strategy, so outbound requests cycle their egress IP instead of pinning one proxy per scope. Migration `117_proxy_pool_rotation.sql` lifts the `UNIQUE(scope, scope_id)` constraint (rebuild via the canonical rename/copy/drop; existing single assignments become 1-element pools) and adds a `proxy_scope_rotation` companion table holding the per-scope strategy + a persisted monotonic round-robin cursor. Strategies: `round-robin` (default, monotonic cursor — never `Math.random`), `random`, and `sticky-per-N-min`. Resolution (`resolveProxyForScopeFromRegistry` / `resolveProxyForConnectionFromRegistry`) now fetches the alive, position-ordered candidate set (unchanged `PROXY_ALIVE_PREDICATE`) and applies the strategy; an empty / all-dead pool still returns `null` — the #6246 fail-closed guard is **untouched** (never falls through to direct egress). Backend + DB only; dashboard pool-builder UI is a follow-up. Regression guard: `tests/unit/proxy-pool-rotation-6365.test.ts` (8, incl. fail-closed + backward-compat). - **feat(providers):** end-to-end **tool/function calling on the native Gemini `/v1beta` endpoint** ([#6222](https://github.com/diegosouzapw/OmniRoute/issues/6222)) — both directions of the Gemini↔OpenAI conversion now preserve tool data (previously silently dropped). Request side: `convertGeminiToInternal` (extracted to its own testable module) maps `tools[].functionDeclarations` → OpenAI `tools`, prior `functionCall` parts → assistant `tool_calls`, and `functionResponse` parts → `tool`-role messages. Response side: `convertOpenAIResponseToGemini` emits `parts[].functionCall {name,args}` from `message.tool_calls`, and the streaming `openAIChunkToGeminiChunk` accumulates fragmented `tool_calls` deltas by index into complete `functionCall` parts. The non-Gemini client paths (Claude, OpenAI-Responses) already preserved tool calls — this closes the gap specific to the native Gemini surface. Regression guard: `tests/unit/v1beta-gemini-tool-calling-6222.test.ts` (6, incl. a streaming SSE round-trip). - **feat(providers):** copilot-m365-web **enterprise / work tier** support ([#6334](https://github.com/diegosouzapw/OmniRoute/issues/6334)) — mirrors the EDU-tier pattern (#6210): `M365ConnectionParams` gains an `agent` field, a new opt-in `M365_ENTERPRISE_OVERRIDES` preset (`agent=work`, `scenario=officeweb`, `licenseType=Premium`) applies via `providerSpecificData.tier="enterprise"` (alias `"work"`), and `agent` is also overridable directly via `providerSpecificData.agent`. `buildWsUrl` was hardcoding `agent="web"` (the one enterprise-distinguishing param with no override path), so a Premium work account handshook then returned an empty stream. The individual and EDU paths are untouched. Kilo's dup flag vs #6210 (EDU tier) was a false positive — different tier. Regression guard: `tests/unit/copilot-m365-enterprise-6334.test.ts` (7). End-to-end confirmation on a real Premium work account is a live-VPS validation follow-up (Hard Rule #18). (thanks @Forcerecon) - **feat(api):** standardized, provider-agnostic **`effort` + `thinking` request params** ([#6241](https://github.com/diegosouzapw/OmniRoute/issues/6241)) — a thin standardization layer over the existing mature per-provider reasoning plumbing (no provider mapper touched). `providerChatCompletionSchema` gains a canonical `effort` (reusing the shared `none/low/medium/high/xhigh` vocabulary — the UI tiers `extra`/`max` collapse onto `xhigh`) and a boolean `thinking`. A pure `normalizeReasoningRequest` (wired once in `src/sse/handlers/chat.ts`, before any reasoning field is read) folds them onto the fields the translators already consume (`reasoning_effort` / `reasoning.effort` / `thinking`), so they fan out to Anthropic / Gemini / xAI / Responses — an explicit client `reasoning_effort` / object-shaped `thinking` always wins (backward-compatible). `/models` additively exposes `supportsThinking` + `effort_tiers` so the frontend can render the toggles (UI component is a follow-up). Regression guard: `tests/unit/effort-thinking-standardization-6241.test.ts` (12). (thanks @Iammilansoni, @shabeer) - **feat(combo):** new **`pipeline` (sequential) combo strategy** ([#6297](https://github.com/diegosouzapw/OmniRoute/issues/6297)) — the 18th routing strategy runs targets **in order**, threading each step's output into the next step's input, with an optional per-step `prompt` (system instruction); only the final step's response is returned. Distinct from `fusion` (parallel fan-out + judge). Implemented as a self-contained `open-sse/services/pipeline.ts` (sibling to `fusion.ts`), dispatched from `combo.ts`; the step list reuses `combo.models` order and reads an optional `prompt` off each target (backward-compatible — ignored by every other strategy). Intermediate steps run non-streaming with tools stripped (complete prose to thread forward); the final step keeps the client's `stream` flag + tools. A failing/empty/unparseable intermediate step fails the whole pipeline explicitly via a sanitized error (never silently swallowed). Kilo's dup flag vs #563 was a false positive (that's model→chain selection; this is a sequential chain). Regression guard: `tests/unit/combo-pipeline-strategy.test.ts` (5). (thanks @ofekbetzalel) - **feat(ci):** `check:test-masking` now flags **inline-reimplemented prod conditions** ([#6348](https://github.com/diegosouzapw/OmniRoute/issues/6348)) — a new **report-only** subcheck (v2, 6A.10 family) catches the wrong-shape contract test: a test that recomputes the condition under test inline instead of importing/exercising the real function (the #6216 class, where `=== 500` → `>= 500` stayed green because the test re-implemented the branch). For each added/modified test file it warns when the file textually duplicates a ≥3-token conditional from a production file touched in the same PR **and** does not import the symbol/module owning it, via a pure, fixture-tested `findReimplementedConditions()` with an allowlist mirroring `assertReductionAllowlist`. Report-only for now (does not fail the gate) — to be promoted to blocking after a triage cycle. Regression guard: `tests/unit/check-test-masking.test.ts` (45). - **feat(sse):** per-connection routing override (native vs CLIProxyAPI) ([#6339](https://github.com/diegosouzapw/OmniRoute/issues/6339)) — the previously-dead `isCliproxyapiDeepModeEnabled` helper is now wired into `resolveExecutorWithProxy`: a single connection can opt itself into the CLIProxyAPI passthrough executor via `providerSpecificData.cliproxyapiMode="claude-native"`, with precedence **connection override > provider `upstream_proxy_config` mode > default**. `resolveExecutorWithProxy` now receives the resolved connection's `providerSpecificData` (threaded from `chatCore.ts`), so one connection can deep-route while the provider's other connections stay native — no DB schema change (the toggle rides in `providerSpecificData`). Also resolves the same-provider mixing ask in #6340. Regression guard: `tests/unit/chatcore-executor-proxy.test.ts` (9). (thanks @RaviTharuma) - **feat(dashboard):** "Add session cookie" modal now shows a prominent **"Open ‹host› →"** link to the provider's own site ([#6268](https://github.com/diegosouzapw/OmniRoute/issues/6268)) — every `-web` cookie-session provider (chatgpt-web, claude-web, gemini-web, kimi-web, lmarena, qwen-web, m365-copilot-web, …) renders a one-click external link (opening the provider's login/home page in a new tab) so operators no longer tab away to retype the URL mid-setup. The host resolves from a pure, unit-tested `resolveWebProviderHost()` (prefers `WEB_COOKIE_PROVIDERS[id].website`, falls back to the registry `baseUrl` origin); non-web providers render exactly as before. Kilo's dup flag vs #6265 (modal-too-small-on-1080p) was a false positive — distinct concern. Regression guard: `tests/unit/resolve-web-provider-host.test.ts` (5). (thanks @chirag127) - **feat(providers):** add **DigitalOcean** AI (serverless inference) as an OpenAI-compatible API-key provider ([#6373](https://github.com/diegosouzapw/OmniRoute/pull/6373)) — base `https://inference.do-ai.run/v1`, wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/digitalocean/`, `src/shared/constants/providers/apikey/inference-hosts.ts`). Regression guard: `tests/unit/digitalocean-provider.test.ts`. (thanks @newnol) - **feat(providers):** add **Huancheng Public API** (`hcnsec`) as an OpenAI-compatible regional provider ([#6410](https://github.com/diegosouzapw/OmniRoute/pull/6410)) — Xinjiang Huancheng Cybersecurity's public LLM platform (base `https://api.hcnsec.cn/v1`, free credits via daily check-ins), wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/hcnsec/`, `src/shared/constants/providers/apikey/regional.ts`). Regression guard: `tests/unit/hcnsec-provider.test.ts`. (thanks @UnrealAryan) - **feat(dashboard):** the web-session credential guide now shows an **"Open {host}" link** ([#6316](https://github.com/diegosouzapw/OmniRoute/pull/6316)) to the provider's sign-in site (derived from the provider `website` via `getProviderWebsiteHost`), so you can jump straight to the page where the cookie/session must be captured. Regression guard: `tests/unit/web-session-provider-link-6316.test.ts`. (thanks @jordansilly77-stack) - **feat(cerebras):** add the **Gemma 4 31B** model (`gemma-4-31b`) to the Cerebras registry + pricing table ([#6331](https://github.com/diegosouzapw/OmniRoute/pull/6331)). Regression guard extends `tests/unit/t28-model-catalog-updates.test.ts`. (thanks @backryun) - **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](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`. ### 🔧 Bug Fixes - **fix(dashboard):** adding a second API key connection for the same provider no longer silently overwrites the first — the Add-API-key modal now derives a unique default connection name (`main`, then `main-2`, `main-3`, …) so the backend name-based upsert can't collide (#6499 — thanks @dilneiss). - **fix(compression):** the session-dedup engine now also deduplicates a large multi-line block repeated **within a single message** (intra-message dedup), not just across turns; the compression-preview API surfaces a `fallbackReason`, and the fusion panel reports how many models were rate-limited vs failed on total-panel failure (#6501 — thanks @chirag127). - **fix(compression):** a stacked-pipeline step naming an unregistered engine now surfaces a `validationErrors` entry instead of silently no-op'ing, so misconfigured pipelines are visible in the preview API (#6506 — thanks @chirag127). - **feat(usage):** add a **Codex reset-credit redemption** flow to the Provider Limits UI ([#6361](https://github.com/diegosouzapw/OmniRoute/pull/6361)) — a `useCodexResetCreditRedemption` hook + `/api/usage/codex-reset-credit` route + `codexResetCredits` lib let you redeem banked Codex reset credits from the quota card. Regression guard: `tests/unit/codex-reset-credits.test.ts`. (thanks @JxnLexn) - **feat(glm):** add **team-plan quota settings** for `glm-cn` connections ([#6351](https://github.com/diegosouzapw/OmniRoute/pull/6351)) — a dedicated `GlmTeamQuotaFields` form section (team quota id / limits) threaded through the Add/Edit connection modals, persisted via `providerSpecificData`, with the GLM usage service reading the team quota. Regression guards: `tests/unit/glm-team-quota.test.ts`, `provider-specific-data-schema.test.ts`. (thanks @hao3039032) - **feat(providers):** add **TinyFish** web-fetch/search support ([#6349](https://github.com/diegosouzapw/OmniRoute/pull/6349)) — a `tinyfish-fetch` executor + `/v1/web/fetch` route + MCP web-fetch tool, registered as a specialty-media provider with request-validation and a search-provider catalog entry. Regression guards: `tests/unit/executor-tinyfish-fetch.test.ts`, `web-fetch-handler.test.ts`, `mcp-web-fetch-tool.test.ts`, `provider-validation-tinyfish.test.ts`. (thanks @dtybnrj) - **fix(cli):** `omniroute launch-codex` now spawns `codex.cmd` through a shell on Windows (the npm `.cmd` shim is unresolvable by bare `spawn` → ENOENT), mirroring the qodercli Windows fix (#6263) ([#6312](https://github.com/diegosouzapw/OmniRoute/pull/6312)). Regression guard: `tests/unit/launch-codex-windows-spawn-6312.test.ts`. (thanks @swingtempo) - **fix(codex):** isolate the **Spark** quota from the shared Codex quota and stabilize the quota UI ordering / hydration so per-scope limits render consistently ([#6336](https://github.com/diegosouzapw/OmniRoute/pull/6336)). Regression guards: `tests/unit/codex-quota-selection-hydration.test.ts`, `provider-limits-ui.test.ts` + 3 more. (thanks @xz-dev) - **feat(api):** add a `hidePaidModels` setting that filters paid-only models out of the `/v1/models` catalog. Regression guard: `tests/unit/models-catalog-hide-paid.test.ts`. (thanks @chirag127) - **fix(api-manager):** the fallback model picker now preserves combos instead of dropping them when a primary model is unavailable ([#6443](https://github.com/diegosouzapw/OmniRoute/pull/6443)). Regression guard: `tests/unit/api-manager-page-static.test.ts`. (thanks @jmengit) - **fix(providers):** recoverable Antigravity / Cloud-Code (Gemini Code Assist) `403` responses ([#6452](https://github.com/diegosouzapw/OmniRoute/pull/6452)) are now classified as a retryable project-config error instead of a terminal account ban, so a fixable project/API-disabled 403 no longer forces a ~1-year cooldown / full OAuth reconnect. Regression guard: `tests/unit/errorclassifier-antigravity-403.test.ts`. (thanks @developerjillur) - **fix(mitm):** `sanitizeHeaders` now redacts `Set-Cookie` response headers so upstream session cookies never leak into logs / diagnostics ([#6451](https://github.com/diegosouzapw/OmniRoute/pull/6451)). Regression guard: `tests/unit/mitm-sanitize-headers.test.ts`. (thanks @developerjillur) - **fix(api):** `/api/compression/preview` now accepts `mode: "caveman"` and correctly handles stacked / zero-compression previews ([#6425](https://github.com/diegosouzapw/OmniRoute/issues/6425)). Regression guard: `tests/unit/api/compression-preview-caveman-and-stacked-6425.test.ts`. (thanks @chirag127) - **feat(providers):** add **Zed** hosted LLM aggregator as a native-app provider ([#6118](https://github.com/diegosouzapw/OmniRoute/pull/6118)) — OAuth sign-in via the Zed hosted flow, registered through the shared provider registry + executor. Regression guards: `tests/unit/zed-oauth-provider.test.ts`, `zed-import-utils.test.ts`, `zed-docker-detect.test.ts`, `mitm-handler-zed.test.ts`. VPS-validated via live operator login (Hard Rule #18). - **fix(oauth):** the Kiro SSO-cache auto-import now **preserves the IDC region** — cross-region Amazon Q / Kiro profiles imported from the SSO cache are no longer collapsed to the default region ([#6113](https://github.com/diegosouzapw/OmniRoute/pull/6113)). Regression guard: `tests/unit/kiro-auto-import-idc-2059.test.ts`. VPS-validated via live operator login (Hard Rule #18). - **fix(dashboard):** passthrough model aliases no longer collide when two namespaced model ids share a last segment (port from 9router#1850, [#6431](https://github.com/diegosouzapw/OmniRoute/pull/6431)). `enx/gpt-5.5` and `enx/codebuddy/gpt-5.5` both auto-generated the alias `gpt-5.5`, so the second model could never be added (the UI just alerted "alias already exists"). Aliases are now disambiguated deterministically — bare last segment when free, then parent-qualified (`codebuddy-gpt-5.5`), then a numeric suffix — while re-adding the exact same model id is still blocked. Regression guard: `tests/unit/passthrough-alias-1850.test.ts`. (thanks @arpicato) - **fix(translator):** preserve a Gemini `functionResponse` co-located with other parts (another `functionCall`, or trailing `text`) in the same content when translating **Gemini → OpenAI** ([#6376](https://github.com/diegosouzapw/OmniRoute/pull/6376)). `convertGeminiContent()` early-returned the tool message on the first `functionResponse` part, dropping any co-located parts; such contents are now pre-split (one tool message per `functionResponse`, emitted first, plus one message for the remaining parts). Regression guard: `tests/unit/gemini-to-openai-function-response.test.ts`. (thanks @warelik) - **fix(headroom):** detect a python interpreter managed by **mise / pyenv / asdf / conda** (port from 9router#2353, [#6382](https://github.com/diegosouzapw/OmniRoute/pull/6382)). Headroom's python probe (`src/lib/headroom/detect.ts`) searched a hardcoded `PATH`, but version managers expose their interpreters via shim dirs that only join `PATH` through interactive-shell activation — which the non-interactive server never runs, so a managed python (≥3.10) was invisible and Headroom reported it missing. The search path now prepends the well-known shim/bin dirs (`~/.local/share/mise/shims`, `~/.pyenv/shims`, `~/.asdf/shims`, `$CONDA_PREFIX/bin`, `~/.local/bin`, respecting `MISE_DATA_DIR`/`PYENV_ROOT`/`ASDF_DATA_DIR` when set), and a new `HEADROOM_PYTHON` env override lets operators point straight at their interpreter (mirroring `HEADROOM_URL`). Still shell-free (`execFileSync`). Regression guard: `tests/unit/headroom-detect.test.ts` (5). (thanks @loopyd) - **fix(executors):** strip the OpenAI-Codex/Claude-CLI `client_metadata` passthrough field for **NVIDIA** requests (port from 9router#1887, [#6411](https://github.com/diegosouzapw/OmniRoute/pull/6411)). NVIDIA's OpenAI-compatible wrapper rejects it with `400 Unsupported parameter`, the same class already handled for `cerebras`/`mistral`; `nvidia` (executor `default`) was missing from the strip allowlist so Codex/Claude-Code passthrough requests 400'd. Regression guard: `tests/unit/executor-default-strip-client-metadata.test.ts` (+nvidia case). (thanks @phidinhmanh) - **fix(translator):** strip the Claude-style `thinking` field for **NVIDIA `z-ai/glm-5.2`** (port from 9router#2023, [#6413](https://github.com/diegosouzapw/OmniRoute/pull/6413)). NVIDIA's OpenAI-compatible wrapper 400s on `thinking` (a Claude-format client routed here leaves a `thinking:{type:"adaptive"}`); the existing strip rule only dropped `reasoning`. Same class already handled for `minimax-m2.7`. Regression guard: `tests/unit/nvidia-minimax-thinking-strip.test.ts` (+glm-5.2 case). (thanks @phidinhmanh) - **fix(translator):** suppress the streamed `` close marker for the **Antigravity IDE** client (port from 9router#1061, [#6415](https://github.com/diegosouzapw/OmniRoute/pull/6415)). On thinking-only turns Antigravity rendered a bare `` as the sole visible content, tripping its loop-detection and wasting requests. Antigravity's UA (`vscode/ (Antigravity/)`) is added to the marker-suppress allowlist (alongside OpenCode); Claude Code / Cursor still get the marker, and `x-omniroute-thinking-marker: on` force-restores it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts`. (thanks @abdofallah) - **fix(executors):** strip nested `reasoning_content` from messages for **Mistral** (port from 9router#1649, [#6417](https://github.com/diegosouzapw/OmniRoute/pull/6417)). Mistral's API returns `422 extra_forbidden` when an assistant message carries `reasoning_content` (replayed thinking from a prior turn, e.g. via the Codex `/responses` path); the generic top-level 400 field-downgrade retry never covered the nested per-message field. `DefaultExecutor` now strips it for provider `mistral` only, so DeepSeek (which requires replayed `reasoning_content`) is unaffected. Regression guard: `tests/unit/mistral-strip-reasoning-content-1649.test.ts`. (thanks @xxy9468615) - **fix(executors):** strip the `client_metadata` passthrough field on the **OpenCode** path (port from 9router#1442, [#6418](https://github.com/diegosouzapw/OmniRoute/pull/6418)). OpenCode upstreams (e.g. `kimi-k2.6` via opencode-go) reject it with `400 "Extra inputs are not permitted, field: 'client_metadata'"`; the DefaultExecutor strip only covered cerebras/mistral and `OpencodeExecutor` extends `BaseExecutor` directly, so nothing removed it there. Regression guard: `tests/unit/opencode-strip-client-metadata-1442.test.ts`. (thanks @yanpaing007) - **fix(executors):** inject the `reasoning_content` echo for the native **Moonshot Kimi** provider (port from 9router#1480, [#6419](https://github.com/diegosouzapw/OmniRoute/pull/6419)). Kimi (executor `default`) is a thinking-mode upstream that 400s with "reasoning_content must be passed back" when a prior assistant turn lacks it; the placeholder injection was only wired into the OpenCode meta-provider, so direct multi-turn Kimi conversations failed. Scoped to `kimi` (gateway-served models matching the thinking-model name pattern are unaffected). Regression guard: `tests/unit/kimi-native-reasoning-injected-1480.test.ts`. (thanks @2220258345) - **fix(executors):** recover from a strict gateway's `context_management: Extra inputs are not permitted` 400 (port from 9router#1468, [#6420](https://github.com/diegosouzapw/OmniRoute/pull/6420)). **Claude Code** always sends a top-level `context_management` field; strict anthropic-compatible gateways reject it. The dedicated context-editing 400-fallback only fired when OmniRoute's own `contextEditing` feature was enabled (default off), so a client-sent field passed through untouched and 400'd. `context_management` is now in the generic reactive field-strip list, so it's stripped-and-retried once regardless of the feature flag (with correct request re-signing for claude-compatible relays). Regression guard: `tests/unit/provider-field-strips.test.ts`. (thanks @ohahe52-dot) - **fix(network):** enable **RFC 8305 Happy Eyeballs** (`autoSelectFamily`) on the direct-egress undici dispatcher (port from 9router#1237, [#6423](https://github.com/diegosouzapw/OmniRoute/pull/6423)). When DNS returns both IPv6 (AAAA) and IPv4 (A) and the IPv6 route is broken (e.g. a NAT64 `64:ff9b::` prefix without routing), undici tried IPv6 first and hung until `ETIMEDOUT` (then a 502 + account lockout), even though `curl` reached the same host. The direct dispatcher now races both families and uses whichever connects first. Proxy paths pin family via `proxyTls` and are unaffected. Regression guard: `tests/unit/direct-dispatcher-pipelining-4580.test.ts`. (thanks @adentdk) - **fix(combo):** round-robin now advances the rotation pointer past the model that **actually served**, not the eagerly-scheduled one (port from 9router#948, [#6428](https://github.com/diegosouzapw/OmniRoute/pull/6428)). With `stickyLimit: 1` (true round-robin), when the scheduled model failed and a _different_ model served via fallback, the counter had already advanced +1 from the scheduled index — so the next request reused the fallback-served model, degrading round-robin into hot-spotting on whichever model was healthy. The pointer now advances to the served index + 1 (mirroring the sticky-limit>1 path). Session-stickiness (#3825) and distribution are preserved. Regression guard: `tests/unit/combo-rr-fallback-advance-948.test.ts`. (thanks @binsarjr) - **fix(sse):** a non-string `model` field is now rejected with a `400` before the resolver, instead of crashing downstream `.toLowerCase()`/`.split()` calls into an empty-body `500` that escapes the error sanitizer ([#6407](https://github.com/diegosouzapw/OmniRoute/issues/6407)). Regression guard: `tests/unit/chat-non-string-model-6407.test.ts`. (thanks @chirag127) - **fix(api):** unknown `/api/*` routes now return a JSON `404` (instead of the dashboard HTML shell) and scalar chat params (`model`/`temperature`/etc.) are validated **before** the provider lookup so malformed requests fail fast with a clear `400` ([#6424](https://github.com/diegosouzapw/OmniRoute/issues/6424), [#6412](https://github.com/diegosouzapw/OmniRoute/issues/6412)). Regression guards: `tests/unit/api/api-catchall-json-404.test.ts`, `tests/unit/chat-early-schema-validation-6412.test.ts`. (thanks @chirag127) - **fix(api):** `/v1/chat/completions` now rejects a non-JSON `Content-Type` with a `400` before parsing the body ([#6414](https://github.com/diegosouzapw/OmniRoute/issues/6414)). Regression guard: `tests/unit/v1-chat-completions-content-type-6414.test.ts`. (thanks @chirag127) - **fix(api):** the `X-OmniRoute-Compression` response header is now echoed on `/v1/chat/completions` and `/v1/completions` ([#6422](https://github.com/diegosouzapw/OmniRoute/issues/6422)). Regression guard: `tests/unit/compression-header-echo-6422.test.ts`. (thanks @chirag127) - **fix(api):** concurrent `GET /v1/models` requests are coalesced into a single catalog build ([#6408](https://github.com/diegosouzapw/OmniRoute/issues/6408)). Regression guard: `tests/unit/v1-models-concurrent-6408.test.ts`. (thanks @chirag127) - **fix(api):** `/v1/completions` now echoes the requested `body.model` in its JSON + streamed responses ([#6429](https://github.com/diegosouzapw/OmniRoute/pull/6429)). Regression guard: `tests/unit/completions-body-model-echo.test.ts`. (thanks @chirag127) - **fix(api):** env-var master keys now see the full `/v1/models` catalog ([#6406](https://github.com/diegosouzapw/OmniRoute/issues/6406)). Regression guard: `tests/unit/models-catalog-envkey-6406.test.ts`. (thanks @chirag127) - **fix(api):** non-streaming `/v1/completions` responses now echo `body.model` aligned with the `X-OmniRoute-Model` header ([#6426](https://github.com/diegosouzapw/OmniRoute/issues/6426)). Regression guard: `tests/unit/v1-completions-model-header-match-6426.test.ts`. (thanks @chirag127) - **fix(api):** unknown `/v1/*` routes now return a JSON `404 not_found` instead of the Next.js dashboard HTML shell ([#6405](https://github.com/diegosouzapw/OmniRoute/issues/6405)). Regression guard: `tests/unit/api/v1-catchall-json-404.test.ts`. (thanks @chirag127) - **fix(api):** the per-connection provider models route now degrades to the shipped catalog when a provider's `/models` endpoint answers with a redirect ([#6267](https://github.com/diegosouzapw/OmniRoute/issues/6267)) — a `qwen-web` import failed with a raw `Redirect blocked … (307)` 503. `safeOutboundFetch` throws `REDIRECT_BLOCKED` on the 307, `getSafeOutboundFetchErrorStatus` maps it to 503, and `buildDiscoveryErrorFallbackResponse` treated every 503 as a hard error — so the non-empty `getModelsByProviderId("qwen-web")` catalog was never surfaced. A models-endpoint redirect is not a fixable-config error (unlike `URL_GUARD_BLOCKED`/`INVALID_URL`, which stay hard errors), so it now falls back to the local/cached catalog before the 503 short-circuit. General fix — covers any config-driven provider that 307s. Regression guard: `tests/unit/provider-models-qwen-web-redirect-6267.test.ts`. (thanks @chirag127) - **fix(api):** the per-connection provider models route (MCP `list_models_catalog` + the dashboard import view) now merges USER-ADDED custom models into its response ([#6247](https://github.com/diegosouzapw/OmniRoute/issues/6247)) — custom models live in the `key_value` namespace `customModels`, which the live REST `/api/v1/models` already merges, but `src/app/api/providers/[id]/models/route.ts` never read `getCustomModels`, so custom models were dropped on both the discovery-success and local_catalog paths. They are now folded into the returned model list (deduped by id, stamped `owned_by: provider`), fixing MCP + the dashboard import view in one place. Regression guard: `tests/unit/provider-models-custom-merge-6247.test.ts`. (thanks @RCrushMe) - **fix(providers):** GitLab Duo tool-calling follow-up turns no longer fail upstream with `422 {"detail":"Validation error"}` (tokens 0/0, rejected pre-inference). The #6234 tool-result-feedback fix serialized the **entire** multi-turn conversation into GitLab's single-file `code_suggestions` (`small_file`) generation endpoint — folded history that turn-N sent as an oversized `current_file.content_above_cursor` **and** duplicated verbatim into `user_instruction`, tripping the AI-Gateway's `small_file` validation guard. The executor now **bounds** that prompt: it keeps system + latest user message + the most-recent tool round (dropping older turns), caps oversized tool results, and stops duplicating the full prompt into `user_instruction` (which now carries only the short latest user message) — while still feeding the most-recent tool result back so the agent continues (`open-sse/executors/gitlab.ts`, [#6220](https://github.com/diegosouzapw/OmniRoute/issues/6220)). The unit test covers the bounding logic; the upstream 422→200 clearing is VPS-only (Hard Rule #18). Regression guard: `tests/unit/gitlab-tool-exchange-bounded-6220.test.ts`. - **fix(i18n):** the provider-detail (`/dashboard/providers/[id]`) connection-status filter labels no longer render as `__MISSING__:All` / `__MISSING__:Active` / `__MISSING__:Error` / `__MISSING__:Banned` / `__MISSING__:CreditsExhausted` in non-English locales (notably pt-BR) ([#6290](https://github.com/diegosouzapw/OmniRoute/issues/6290)). Root cause was **not** the namespace mismatch the issue guessed — the `providers.filter*` keys resolve correctly in `en.json`; the debt lived in the locale mirrors (`src/i18n/messages/*.json`), where these five keys carried the `__MISSING__:` sync sentinel in ~15 locales and were absent entirely in ~26 others, so next-intl found the key and echoed the sentinel verbatim. All 40 non-English/-Chinese mirrors now ship real translations for the five `providers.filter*` labels. Regression guard: `tests/unit/i18n-provider-filter-keys-6290.test.ts`. (thanks @diegosouzapw) - **fix(providers):** the `copilot-m365-web` streaming executor now emits `debug`-level WebSocket diagnostics ([#6210](https://github.com/diegosouzapw/OmniRoute/issues/6210)) — the outbound WS URL (with the `access_token` **redacted** via `redactWsUrl()`), handshake success/failure, and each received SignalR frame's `type`/`target`. Previously the streaming path logged nothing, so an empty `content:null` response (the M365 Education / Starter tier symptom fixed in #6234) was undiagnosable even at `APP_LOG_LEVEL=debug`. The change is debug-level and side-effect-free — it does not alter streaming behavior or the frame parser, and the token never reaches the logs. Regression guard: `tests/unit/copilot-m365-web-logging-6210.test.ts` (thanks @qpeyba) - **fix(resilience):** a round-robin combo no longer returns `503 all upstream accounts are unavailable` when a compatibility-rejected target is actually healthy ([#6238](https://github.com/diegosouzapw/OmniRoute/issues/6238)). `filterTargetsByRequestCompatibility` drops request-incompatible targets (tool/vision/structured-output unsupported, or below the required context window) **before** any availability check runs, and its `compatible.length === 0` safety net only fired when _all_ targets were filtered — not when the kept targets later all turned out runtime-unavailable (circuit-open / cooldown / no credentials). So a combo could 503 while a compat-rejected-but-healthy provider sat unused. `handleRoundRobinCombo` now keeps the compat-rejected set and, when every compat-kept target was skipped without a single real attempt, probes those rejected targets as a **last-resort fallback tier** (via the new pure `open-sse/services/combo/comboCompatFallback.ts`) before crystallizing the 503. Regression guard: `tests/unit/combo-roundrobin-compat-fallback-6238.test.ts`. (thanks @ThongAccount) - **fix(startup):** best-effort self-heal for a corrupted Turbopack dev cache on Windows ([#6289](https://github.com/diegosouzapw/OmniRoute/issues/6289)). On Windows, `pnpm dev` can fail at startup when Turbopack `mmap`s a persistent-cache SST file and the OS refuses the mapping (`os error 1455` — "paging file too small"), which Turbopack surfaces as a misleading `Module not found: Can't resolve '@/shared/utils/machine'`. This is a **known upstream Turbopack cache-corruption bug — not our code**. The dev launcher (`scripts/dev/run-next.mjs`) now wraps `nextApp.prepare()` and, when it rejects with that signature (`isTurbopackCacheCorruption` in the new `scripts/dev/turbopackCacheHeal.mjs`), purges `.build/next/**/cache/turbopack` and retries **once** with a clear log. **Caveat — best-effort only:** the corruption often surfaces as a runtime overlay rather than a `prepare()` rejection, so this cannot always intercept it; the reliable remedy remains manually deleting the Turbopack cache dir. Regression guard: `tests/unit/turbopack-cache-heal-6289.test.ts`. (thanks @chirag127) - **fix(providers):** qodercli PAT auth no longer fails with `spawn qodercli ENOENT` on Windows ([#6263](https://github.com/diegosouzapw/OmniRoute/issues/6263)) — `spawnQoderCli` spawned the bare `qodercli` name with `shell:false` and an unenriched env, so the npm `.cmd` wrapper under `%APPDATA%\npm` (a user-PATH directory) was never resolved. It now resolves the absolute `.cmd`/`.exe` path through the existing `getCliRuntimeStatus("qoder")` resolver in `src/shared/services/cliRuntime.ts` (memoized), spawns with `shell` when the target is a `.cmd`/`.bat`, and uses the cliRuntime-enriched env (PATH + PATHEXT + APPDATA); the ENOENT error now lists the searched paths plus the `CLI_QODER_BIN` override. End-to-end spawn on a real Windows host is host-only (Hard Rule #18); the path-resolution logic is unit-tested. Regression guard: `tests/unit/qodercli-windows-resolve-6263.test.ts`. (thanks @chirag127) - **fix(sse):** the reasoning-token buffer no longer inflates **probe-sized `max_tokens`** ([#6274](https://github.com/diegosouzapw/OmniRoute/issues/6274)) — Claude Code's `/model` capability check sends `max_tokens: 1`, but for a thinking-capable model with a large output cap (e.g. `glm-5.2`) the #3587 headroom heuristic (`max(current + 1000, ceil(current * 1.5))`) rewrote it to `1001` and forwarded that upstream, wasting tokens on a request that was never a genuine reasoning budget. `resolveReasoningBufferedMaxTokens()` (`open-sse/services/reasoningTokenBuffer.ts`) now short-circuits and returns the caller's value verbatim when it is below the new `REASONING_BUFFER_MIN_TRIGGER` (256) threshold — a tiny explicit limit is a probe, not a reasoning request. Real budgets still receive the #3587 headroom unchanged, and the guard runs after the existing capability checks so unknown / non-reasoning models keep returning `null`. Regression guard: `tests/unit/reasoning-token-buffer-6274.test.ts`. (thanks @brightfiscalband) - **fix(cli):** `omniroute reset-password` now works as a real subcommand, and password resets over piped (non-TTY) stdin actually apply ([#6261](https://github.com/diegosouzapw/OmniRoute/issues/6261), [#6258](https://github.com/diegosouzapw/OmniRoute/issues/6258)). Two coupled defects: (1) **#6261** — `bin/omniroute.mjs` routed everything through Commander with only two pre-Commander bypasses (`--mcp`, `reset-encrypted-columns`), so `omniroute reset-password` was rejected as an unknown command; only the separate `omniroute-reset-password` bin worked, while the docs falsely advertised the subcommand (incl. a bogus "legacy alias still works"). A pre-Commander bypass mirroring `reset-encrypted-columns` now dynamically imports `bin/reset-password.mjs` (which self-executes) before Commander parses; the three doc lines were corrected. (2) **#6258** — `bin/reset-password.mjs` issued two sequential `rl.question` prompts; under piped stdin the second read never settled at EOF, so `main()` never reached `resetManagementPassword` and the reset was a silent no-op (both prompts printed, no success, password unchanged). The CLI now detects non-TTY stdin and reads it once (first line = password, second line = confirm if present, else reused), adds a `--password-stdin` flag (entire stdin is the password, no confirmation), and exits `0` explicitly so the success line always flushes; interactive TTY behavior is unchanged. Regression guard: `tests/unit/reset-password-cli-6261-6258.test.ts` (3). (thanks @chirag127) - **fix(db):** the mass-migration **safety abort** now tells the operator how to bypass it and stops flooding the log ([#6260](https://github.com/diegosouzapw/OmniRoute/issues/6260)) — after restoring a backup that wiped the migration tracking table, `runMigrations()` threw the abort on every downstream `ensureDbInitialized()`, re-logging the full banner 11+ times, and the message never mentioned the existing `OMNIROUTE_MAX_PENDING_MIGRATIONS` escape hatch. The abort text now appends a bypass hint (set `OMNIROUTE_MAX_PENDING_MIGRATIONS=0` in `server.env` / `DATA_DIR/.env`), and a new `MigrationSafetyAbortError` is memoized so repeated calls in the same process throw the same instance and emit a single concise line instead of the full cascade. Regression guard: `tests/unit/migration-safety-abort-6260.test.ts`. (thanks @chirag127) - **fix(auth):** importing a **distinct** Codex/ChatGPT OAuth `auth.json` is no longer falsely rejected as "already exists" when it belongs to a different user in the same workspace ([#6301](https://github.com/diegosouzapw/OmniRoute/issues/6301)). `findExistingCodexConnection` (in `src/lib/oauth/utils/codexAuthImport.ts`) deduped **only** on `providerSpecificData.workspaceId === accountId`, where `accountId` is the shared `chatgpt_account_id`/`tokens.account_id` — so two members of the same ChatGPT Team collapsed onto a single connection (409 `duplicate_account`). The id_token's `https://api.openai.com/auth` claim carries a per-user `chatgpt_user_id` alongside the workspace id (the device-flow path already persisted it as `chatgptUserId`, but the import path did not). Now `parseAndValidateCodexAuth` extracts `userId` (`chatgpt_user_id` → `user_id` → JWT `sub`) into `ParsedCodexAuth`, the create/update paths persist `chatgptUserId` in `providerSpecificData` (mirroring `codex.ts`), and dedup keys on `workspaceId` **AND** `chatgptUserId` — with a backward-compat fallback to legacy accountId-only matching when no stored connection for that workspace records a `chatgptUserId`, so genuinely-same accounts still dedup. Regression guard: `tests/unit/codex-auth-import-userid-dedup-6301.test.ts` (4). (thanks @anungma) - **fix(providers):** importing models for the **venice-web** provider no longer fails with a red "Provider venice-web does not support models listing" ([#6269](https://github.com/diegosouzapw/OmniRoute/issues/6269)). `venice-web` is a web-cookie provider with an executor but no upstream `/v1/models` endpoint and no registry `models`, so the models route fell through to the tail `400`. Mirroring the `jules`/`linkup-search`/`ollama-search` fix (#5569), it now ships a static local catalog entry in `src/lib/providers/staticModels.ts` — seeding the current Venice lineup (`venice-uncensored`, `llama-3.3-70b`, `qwen3-235b`, `qwen3-4b`, `deepseek-r1-671b`; Venice rotates its catalog, see docs.venice.ai/models/overview) — so the route returns `200` with `source:"local_catalog"`, `intentional:true`. Regression guard: `tests/unit/static-models-venice-web-6269.test.ts`. (thanks @chirag127) - **fix(api):** the specialty model catalogs (`/v1/embeddings`, `/v1/images`, `/v1/music`, `/v1/videos` model lists) are now derived from the **unified catalog filtered by a predicate** (`getSpecialtyModelsResponse`) instead of ad-hoc per-route logic, so they consistently respect active-credential visibility and stay in sync with the main catalog ([#6303](https://github.com/diegosouzapw/OmniRoute/pull/6303)). Regression guard: `tests/unit/specialty-model-catalog-routes.test.ts`. (thanks @makcimbx) - **fix(api):** the agent-bridge server route now resolves the MITM manager via a **dynamic `import("@/mitm/manager.runtime")`** so Turbopack does not statically pull the stub (or over-bundle the manager), and the agent-skills generator anchors its output base path with `path.join(process.cwd(), …)` so Turbopack's static analyzer stops tracing the whole project root ([#6329](https://github.com/diegosouzapw/OmniRoute/issues/6329), [#6366](https://github.com/diegosouzapw/OmniRoute/pull/6366)). Regression guard: `tests/unit/agent-bridge-server-route-dynamic-import.test.ts`. (thanks @Iammilansoni) - **fix(api):** internal probes (combo-test, cloud-sync verify) now pick a **management-scoped / allow-all API key** instead of naively grabbing `getApiKeys()[0]` — a restricted `self:usage` first row made the probe fail with "Model X is not allowed for this API key" even when the combo path was healthy (`pickApiKeyForInternalUse` in `src/lib/db/apiKeys.ts`). The API-manager model editor also falls back to `/api/models?all=true` when `/v1/models` is catalog-protected ([#6372](https://github.com/diegosouzapw/OmniRoute/pull/6372)). Regression guard: `tests/unit/pick-internal-api-key-6372.test.ts`. (thanks @jmengit) - **fix(live-ws):** the Live Dashboard WebSocket server now **rejects on bind failure** (e.g. `EADDRINUSE` when the API bridge already holds the port) instead of letting the error surface as an unhandled `error` event that crash-loops the process — the `error` listener is attached to `wss` (not `server`) and releases the EventBus subscription on a failed start ([#6324](https://github.com/diegosouzapw/OmniRoute/issues/6324)). Regression guard: `tests/unit/live-ws-eaddrinuse-6324.test.ts`. (thanks @vinayakkulkarni) - **fix(dashboard):** the Home provider-topology widget now trusts the live provider-metrics snapshot — it uses `topology.errorProvider` and live `activeRequests` directly instead of re-deriving state from a stale `lastErrorAt` or applying a frontend timeout filter, so the topology reflects real-time provider health ([#6322](https://github.com/diegosouzapw/OmniRoute/pull/6322)). Regression guard: `tests/unit/home-provider-topology-live-state.test.ts`. (thanks @xz-dev) - **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](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). - **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 `` 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(sse):** tool-call function schemas with a root `type: null` are now coerced to `type: "object"` before dispatch ([#6359](https://github.com/diegosouzapw/OmniRoute/issues/6359)) — clients like the Codex app emit `parameters: { type: null, ... }` for some tools, which OpenAI-compatible upstreams reject with `400 Invalid schema for function '...': schema must be a JSON Schema of 'type: "object"', got 'type: null'`, failing the whole request. `toolSchemaSanitizer` already stripped the null; it now re-adds the mandatory root `"object"` type (and empty `properties`/open `additionalProperties` when absent). Combinator roots (`anyOf`/`oneOf`/`allOf`) and explicit root types are left untouched. Regression guard: 5 new cases in `tests/unit/tool-schema-sanitizer.test.mjs`. - **fix(docker):** AgentBridge no longer fails to start on npm/Electron/VPS installs with "MITM manager stub reached at runtime" ([#6344](https://github.com/diegosouzapw/OmniRoute/issues/6344)) — v3.8.45 flipped the production bundler default to Turbopack, but `next.config.mjs` aliased `@/mitm/manager` to its Docker-only degraded stub **unconditionally**. That was harmless while Docker (which sets the alias intentionally for #3390 graceful degradation) was the sole Turbopack consumer, but once every artifact built with Turbopack the stub shipped to all non-Docker users and `startMitm` threw on the first Agent-Bridge start. The alias is now opt-in via `OMNIROUTE_MITM_STUB=1` (set only by the Dockerfile) through the shared `scripts/build/mitm-stub-flag.mjs` helper; default builds bundle the real manager. Regression guard: `tests/unit/mitm-stub-alias-6344.test.mjs` (4). - **fix(proxy):** stop the v3.8.44 proxy regression that leaked the real IP and disabled healthy proxies ([#6246](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`. - **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(security):** the doubao synthetic device-id generator now derives its digits via an unbiased crypto-random draw (rejection sampling over `crypto.randomBytes()`) instead of a `% 10` reduction, closing a CodeQL `js/biased-cryptographic-random` finding. - **fix(agentSkills):** the GitHub-skills generator now resolves `outputDir` to an absolute path before writing, fixing a regression introduced by #6366 (relative-to-cwd base path) that could write generated skill files to the wrong directory. - **fix(security):** `/api/keys/{id}/devices` now checks the HTTP method before auth/validation, returning a `405` for non-GET/DELETE verbs instead of a misleading `401`/`500` (closes a `dast-smoke` QUERY-method finding). - **fix(quality):** clear the last 2 heavy quality-gate reds on the release tip (cycle pre-flight). - **fix(mitm):** the test suite and CI can never mutate the OS trust store — `OMNIROUTE_SKIP_SYSTEM_TRUST=1` is set globally for tests/CI so `installCert`/`uninstallCert`/`installTproxyCa` skip the privileged OS dispatch ([#6310](https://github.com/diegosouzapw/OmniRoute/pull/6310); full detail is under the [3.8.45] section below — this branch received it via the parallel-cycle sync-back). - **fix(api):** `POST /api/github-skills` now Zod-validates its request body; documented the new quality-gate env vars and pinned the merge-integrity GitHub Actions to a commit SHA. - **fix(skills):** generate the missing `omni-github-skills` registry entry and align the agent-skills catalog-count tests (follow-up to #6186). - **fix(quality):** clear the cycle's 11 net-new ESLint errors and make `validate-release-green` suppressions-aware. - **fix(security):** proxy-pool `random` rotation now selects via `crypto.randomInt` instead of `Math.random` — silences the post-release CodeQL `js/insecure-randomness` alerts (#698/#699) that flagged `Math.random` flowing into the selected proxy's credentials. Load-balancing selection is not a secret, but the crypto source is unbiased and clears the alert at the origin (#6365 follow-up). ### 📝 Maintenance - **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) - **chore(release):** ~50 commits on this branch are v3.8.45 pre-flight/hardening fixes and CI-perf work that landed here via the parallel-cycle sync-back (`sync-next-cycle.mjs`, Hard Rule #21) **after** the `v3.8.45` git tag was cut, and are already fully documented under the **[3.8.45]** section below — listed here only so the per-cycle commit-coverage check (`npm run release:uncovered`) doesn't flag them as gaps. Provider/catalog/UX/backend fixes: #6041, #6078, #6108, #6135, #6148, #6149, #6154, #6158, #6161, #6162, #6163, #6164, #6165, #6170, #6177, #6178, #6181, #6186, #6187, #6191, #6193, #6194, #6195, #6200, #6205, #6208, #6209, #6211, #6213, #6223, #6224, #6225, #6226, #6227, #6228, #6229, #6230, #6235, #6291, #6292. CI/release-pipeline work: #6167, #6203, #6214, #6215, #6218, #6273, #6275, #6283, #6284, #6285, #6300, #6305. - **chore(release):** additional zero-ref release-cycle plumbing on this branch, kept out of `release:uncovered` on purpose (no `#N` in the commit subject to cite): opening the v3.8.46 cycle, opening/closing the v3.8.45 cycle, the finalized [3.8.45] CHANGELOG i18n sync-back to 42 mirrors, the v3.8.45 cognitive/cyclomatic and file-size drift rebaselines, ESLint stale-suppression pruning (4,273 → 4,233), and clearing test-masking/docs-all pre-flight reds for v3.8.45. ### ⚡ Performance & Infrastructure - **perf(release-green):** the pre-flight validator (`scripts/quality/validate-release-green.mjs`) now runs its 4 slow suites (unit / vitest / integration / pack-artifact) **concurrently** via `Promise.all` — pre-flight wall time drops from ~the sum of the suites to ~the slowest one (~30min saved per round; Phase 0 was the nº1 bottleneck of the v3.8.45 release benchmark, 2h54 of 6h34 e2e). Guard: `tests/unit/validate-release-green.test.ts` ("runs the slow suites CONCURRENTLY"). ([#6319](https://github.com/diegosouzapw/OmniRoute/pull/6319)) - **fix(ci):** `scripts/release/sync-next-cycle.mjs` — two defects found live in its first production run (v3.8.45 Phase 5): (1) the `git()` helper's default 1 MiB `maxBuffer` crashed with `ENOBUFS` on `git show origin/main:CHANGELOG.md` (the CHANGELOG alone is >1 MiB) — widened to 64 MiB; (2) the i18n resync only propagated the `[NEXT]` (TBD) section, leaving the just-shipped finalized section as "— TBD" in all 42 mirrors — it now also syncs `[prevVersion]` bounded by the heading below it (new exported pure helper `versionAfter`). Guards: +5 tests in `tests/unit/sync-next-cycle.test.ts` (8/8). ([#6327](https://github.com/diegosouzapw/OmniRoute/pull/6327)) - **test(ci):** concurrency-sensitive flaky tests are **quarantined into a serial pass** (`tests/unit/serial/`, `--test-concurrency=1`, appended to every unit runner incl. sharded variants — the serial pass is sharded too so concurrent shard jobs never self-collide). Initial set: `glm-coding-plan-monthly-3580`, `quota-division-blocks`, `provider-health-autopilot`, `combo-health-autopilot` — the class behind the ~28min CI wedges/re-runs (two live 1h+ wedges cancelled during this PR's own validation). Discovery + TIA gates track the new glob; systemic root cause (async logger writing after teardown) tracked in [#6360](https://github.com/diegosouzapw/OmniRoute/issues/6360). Guard: `tests/unit/test-serial-quarantine.test.ts` (4). ([#6347](https://github.com/diegosouzapw/OmniRoute/pull/6347)) ### 🙌 Contributors Thanks to everyone whose work landed in v3.8.46: | Contributor | PRs / Issues | | -------------------------------------------------------------- | ---------------------- | | [@2220258345](https://github.com/2220258345) | direct commit / report | | [@abdofallah](https://github.com/abdofallah) | direct commit / report | | [@adentdk](https://github.com/adentdk) | direct commit / report | | [@aleksesipenko](https://github.com/aleksesipenko) | direct commit / report | | [@anki1kr](https://github.com/anki1kr) | direct commit / report | | [@anungma](https://github.com/anungma) | direct commit / report | | [@arpicato](https://github.com/arpicato) | direct commit / report | | [@backryun](https://github.com/backryun) | #6248 | | [@binsarjr](https://github.com/binsarjr) | direct commit / report | | [@brightfiscalband](https://github.com/brightfiscalband) | direct commit / report | | [@chirag127](https://github.com/chirag127) | #6501, #6506 | | [@developerjillur](https://github.com/developerjillur) | direct commit / report | | [@dilneiss](https://github.com/dilneiss) | #6499 | | [@dtybnrj](https://github.com/dtybnrj) | direct commit / report | | [@Forcerecon](https://github.com/Forcerecon) | direct commit / report | | [@hao3039032](https://github.com/hao3039032) | direct commit / report | | [@Iammilansoni](https://github.com/Iammilansoni) | #6150, #6245 | | [@jmengit](https://github.com/jmengit) | direct commit / report | | [@jordansilly77-stack](https://github.com/jordansilly77-stack) | direct commit / report | | [@JxnLexn](https://github.com/JxnLexn) | direct commit / report | | [@KooshaPari](https://github.com/KooshaPari) | #6166 | | [@loopyd](https://github.com/loopyd) | direct commit / report | | [@luweiCN](https://github.com/luweiCN) | #6221 | | [@makcimbx](https://github.com/makcimbx) | direct commit / report | | [@muflifadla38](https://github.com/muflifadla38) | direct commit / report | | [@newnol](https://github.com/newnol) | direct commit / report | | [@ofekbetzalel](https://github.com/ofekbetzalel) | direct commit / report | | [@ohahe52-dot](https://github.com/ohahe52-dot) | direct commit / report | | [@phidinhmanh](https://github.com/phidinhmanh) | direct commit / report | | [@powellnorma](https://github.com/powellnorma) | direct commit / report | | [@qpeyba](https://github.com/qpeyba) | direct commit / report | | [@RaviTharuma](https://github.com/RaviTharuma) | direct commit / report | | [@RCrushMe](https://github.com/RCrushMe) | direct commit / report | | [@rianonehub](https://github.com/rianonehub) | #6134, #6204 | | [@serverless83](https://github.com/serverless83) | #6212 | | [@swingtempo](https://github.com/swingtempo) | direct commit / report | | [@tenshiak](https://github.com/tenshiak) | direct commit / report | | [@ThongAccount](https://github.com/ThongAccount) | direct commit / report | | [@UnrealAryan](https://github.com/UnrealAryan) | direct commit / report | | [@vinayakkulkarni](https://github.com/vinayakkulkarni) | direct commit / report | | [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #6233 | | [@warelik](https://github.com/warelik) | direct commit / report | | [@xxy9468615](https://github.com/xxy9468615) | direct commit / report | | [@xz-dev](https://github.com/xz-dev) | direct commit / report | | [@yanpaing007](https://github.com/yanpaing007) | direct commit / report | | [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- ## [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 `` 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 `` 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`/`` 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 `. - **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 | --- ## [3.8.44] — TBD ### ✨ New Features - **feat(resilience):** throttle upstream quota fetches on the per-request preflight path ([#6009](https://github.com/diegosouzapw/OmniRoute/issues/6009)) — a new global min-interval gate (`open-sse/services/quotaFetchThrottle.ts`) spaces the actual network calls made by the Codex quota fetcher so that many accounts on one IP no longer fetch quota in the same second (which, per `router-for-me/CLIProxyAPI#2385`, can get a Codex OAuth token revoked). Complements the existing bulk-sync spacing (`PROVIDER_LIMITS_SYNC_SPACING_MS`) which already serialized the periodic provider-limits sync — this covers the concurrent combo/preflight path it didn't. Cache hits are never delayed; fail-open (only ever awaits a timer). Configurable via `OMNIROUTE_QUOTA_FETCH_MIN_INTERVAL_MS` (default 250ms, clamped 0..5000; `0` disables). Regression guard: `tests/unit/quota-fetch-throttle-6009.test.ts` (5). (thanks @powellnorma) - **feat(autoCombo):** add **per-request Auto-Combo controls** via two headers ([#6024](https://github.com/diegosouzapw/OmniRoute/issues/6024) / [#6025](https://github.com/diegosouzapw/OmniRoute/issues/6025) / [#6023](https://github.com/diegosouzapw/OmniRoute/issues/6023)) — `X-OmniRoute-Mode` steers an `auto` combo's scoring for a single request (friendly presets `fast`/`balanced`/`quality`/`cheap`/`reliable`/`offline` **or** a raw mode-pack name; `balanced` forces the default weights), and `X-OmniRoute-Budget` sets a hard per-request USD cost ceiling. Both override the combo's stored config only for the request that carries them; unknown/garbage values are ignored so the saved config is preserved. The resolvers are pure (`open-sse/services/autoCombo/requestControls.ts`) and feed the engine's existing `config.modePack` / `config.budgetCap` inputs — no engine changes. Regression guard: `tests/unit/auto-combo-request-controls-6024.test.ts` (5). (thanks @chirag127) - **feat(providers):** add the **Kenari** OpenAI-compatible gateway (BYOK). Regression guard: `tests/unit/kenari.test.ts`. (thanks @doedja) - **feat(models):** add `claude-sonnet-5` to the Antigravity model catalog (alias mapping in `antigravityModelAliases.ts`) ([#6103](https://github.com/diegosouzapw/OmniRoute/pull/6103)). Regression guard: `tests/unit/antigravity-model-aliases.test.ts`. (thanks @anki1kr) - **feat(api):** add `/v1/ocr` endpoint (Mistral OCR), an OCR provider category, and Mistral moderation support. ([#5950](https://github.com/diegosouzapw/OmniRoute/pull/5950)) (thanks @waguriagentic) - **Discovery tool (Phase 2):** add the `discoveryResults` DB module (CRUD over the `discovery_results` table, migration 074) and wire the opt-in provider-discovery service to persist and read findings through it (`persistDiscoveryResult`, `getDiscoveryResults`, `getDiscoveryResultById`, `markVerified`, `deleteDiscoveryResult`) with `(provider, method, endpoint)` upsert de-duplication. Adds the `/api/discovery/*` HTTP surface — `GET /results`, `GET|DELETE /results/:id`, `POST /scan`, `POST /verify/:id` — under **strict loopback-only** authorization (`/api/discovery/` is in `LOCAL_ONLY_API_PREFIXES` and is NOT manage-scope-bypassable, so the `scan` route's outbound probes can never be reached from a tunnel/remote origin). Adds a **dashboard UI tab** (Tools → Discovery, `/dashboard/discovery`) to run scans and review, verify, or delete findings. The service stays **opt-in / default-off**. ([#5939](https://github.com/diegosouzapw/OmniRoute/pull/5939)) - **feat(api):** expose a read-only provider plugin manifest at `GET /api/v1/provider-plugin-manifest` for sidecar/relay discovery. ([#6001](https://github.com/diegosouzapw/OmniRoute/pull/6001)) (thanks @KooshaPari) - **feat(sidecar):** advertise the provider manifest URL to Bifrost/CLIProxyAPI via the `X-OmniRoute-Provider-Manifest-Url` header (`OMNIROUTE_PROVIDER_MANIFEST_URL`). ([#6007](https://github.com/diegosouzapw/OmniRoute/pull/6007)) (thanks @KooshaPari) - **feat(autoCombo):** add a latency/speed-optimized routing mode (shared `rankBySpeed` scoring core) plus the `omniroute_pick_fastest_model` MCP tool. ([#6011](https://github.com/diegosouzapw/OmniRoute/pull/6011)) (thanks @KooshaPari) - **feat(providers):** refresh The Old LLM (Free) model catalog ([#5181](https://github.com/diegosouzapw/OmniRoute/issues/5181)) — seed the current free `/api/chatgpt` tier (GPT-5/5.1/5.2/5.3/5.4, o3/o4-mini, Gemini 3 Pro / 2.5 Pro / 2.0 Flash / 1.5 Flash, Claude 4.6 Opus/Sonnet & 4.5 Haiku, GPT-4o, Grok 4, DeepSeek V3/R1, Sonar Pro) while keeping the legacy alias IDs for saved-preference compatibility. Also fixes a latent routing bug: `mapModel()` now passes known upstream IDs through unchanged, so Gemini/o-series/Grok/DeepSeek/Sonar models no longer silently collapse onto `GPT_5_4`. Regression guard: `tests/unit/theoldllm-model-refresh-5181.test.ts`. (thanks @WslzGmzs) - **feat(resilience):** surface Codex **banked reset credits** per connected account ([#5199](https://github.com/diegosouzapw/OmniRoute/issues/5199)) — the Codex quota parsers (`buildCodexUsageQuotas`, `parseCodexUsageResponse`) now additively read `rate_limit_reset_credits.available_count` (+ optional `rate_limit_reached_type`) from the `/wham/usage` payload OmniRoute already fetches, and the provider-limits dashboard renders a **"Banked Reset Credits"** row when a positive count is present. Display-only and **fail-open** — the field is eligibility-gated, so accounts without it are unaffected (parsers never throw on absent/garbage shapes); redemption (an unofficial mutating endpoint) is intentionally out of scope. Regression guard: `tests/unit/codex-banked-reset-credits-5199.test.ts` (8). (thanks @ofekbetzalel) - **feat(providers):** add sign-up geo-restriction notices for **SenseNova** and **StepFun** ([#5462](https://github.com/diegosouzapw/OmniRoute/issues/5462)) — the provider add-form now warns that SenseNova's console appears to require a Chinese (+86) phone number with no documented international path, and that StepFun's default endpoint is its China platform while a global StepFun Open Platform (`platform.stepfun.ai`, operated by Sparkling AI Pte. Ltd., Singapore) with email/Google/Discord login exists for international users. Informational `notice` only — neither provider is disabled. Regression guard: `tests/unit/regional-provider-cn-notices-5462.test.ts`. (thanks @chirag127) - **feat(usage):** add on-demand period-scoped usage-data reset (Settings → System Storage) with a purge API and time-window selector. ([#5831](https://github.com/diegosouzapw/OmniRoute/pull/5831)) - **feat(claude-code):** add an opt-in auto-permission classifier compat mode (off/auto/always) for Claude Code, toggleable from the CLI Code settings. ([#5810](https://github.com/diegosouzapw/OmniRoute/pull/5810)) - **feat(providers):** add optional client-identity header profiles for compatible nodes — preset User-Agent/fingerprint headers (e.g. matching a known CLI) merged into the existing customHeaders field. ([#5812](https://github.com/diegosouzapw/OmniRoute/pull/5812)) - **feat(build):** add a backend-only fast build mode (`scripts/build/build-next-isolated.mjs` + `backendOnlyPages.mjs`) that skips compiling the dashboard frontend pages, cutting local/CI build time for backend-only changes. ([#6119](https://github.com/diegosouzapw/OmniRoute/pull/6119) — thanks @artickc) - **feat(minimax):** extract MiniMax M3's raw `...` leakage into `reasoning_content` on the 8 OpenAI-format provider tiers, leaving the Claude-format `minimax`/`minimax-cn` tiers untouched (they already report reasoning correctly). ([#6073](https://github.com/diegosouzapw/OmniRoute/pull/6073) — thanks @KooshaPari) - **feat(services):** promote **Bifrost** (`@maximhq/bifrost` — Go AI-gateway) from an env-only relay sidecar to a first-class embedded/supervised service, matching the existing cliproxy/9router model — installer, bootstrap `SERVICES[]` entry, migration 113 DB seed, 7 lifecycle API routes under `/api/services/bifrost/` (loopback-only), a dashboard tab, and relay auto-wiring that defaults `BIFROST_BASE_URL` to the supervised port when running. Implements item #2 of #5670; the broader RouterBackend contract (items #1, #3-#5) stays out of scope. ([#5817](https://github.com/diegosouzapw/OmniRoute/pull/5817), part of [#5670](https://github.com/diegosouzapw/OmniRoute/issues/5670)) - **feat(services):** add **Mux** (`coder/mux` — local agent-orchestration daemon) as a fourth-tier embedded service on the existing `ServiceSupervisor` framework — npm-based installer, `bootstrap.ts` registration, migration 113 DB seed, 7 lifecycle API routes under `/api/services/mux/` (loopback-only, defense-in-depth bind to 127.0.0.1), and a dashboard tab reusing the shared service-management components. ([#6034](https://github.com/diegosouzapw/OmniRoute/pull/6034)) - **feat(xai):** surface Grok/xAI usage on the quota dashboard via local `usageHistory` aggregation (`getXaiUsage`) — since xAI exposes no per-account quota API, this sums tokens routed to the connection from `usage_history` and reports them as a cumulative, uncapped quota, mirroring the existing Xiaomi MiMo self-track pattern. ([#5806](https://github.com/diegosouzapw/OmniRoute/pull/5806)) - **feat(minimax):** extract MiniMax M3's raw `...` tags into a separate `reasoning_content` field on the 8 provider tiers that register M3 with `format:"openai"` (trae, huggingchat, bazaarlink, ollama-cloud, opencode, cline, opencode-zen, codebuddy-cn) — previously the thinking text leaked directly into `content`. Reuses the existing `extractThinkingFromContent` primitive, extending its allowlist with a minimax-m3-only pattern; the two direct minimax/minimax-cn tiers are untouched since they already surface reasoning natively over Anthropic's Messages format. (Inspired by 9router#2231.) ([#6050](https://github.com/diegosouzapw/OmniRoute/pull/6050) — thanks @KooshaPari) - **feat(i18n):** auto-detect the browser language on first visit — a pure `detectBrowserLocale()` matcher (exact match, `zh-HK`/`zh-MO` folded to `zh-TW`, language-prefix match, else `null`) plus a client-only `LocaleAutoDetect` component mounted once in the root layout. When no locale cookie is set yet, it reads `navigator.languages`, computes a match against the supported locales, and persists it via the same cookie/localStorage writer `LanguageSelector` already used (extracted to `shared/lib/persistLocale.ts`). (Inspired by 9router#1324.) ([#5979](https://github.com/diegosouzapw/OmniRoute/pull/5979)) - **feat(cli-tools):** add **CodeWhale** — the actively-maintained successor to DeepSeek TUI (same author, renamed project) — as a dual dashboard entry alongside the existing "deepseek-tui" catalog entry, so existing DeepSeek TUI users keep a working card while new users are steered to CodeWhale. New `/api/cli-tools/codewhale-settings` route writes `~/.codewhale/config.toml` and keeps the legacy `~/.deepseek/config.toml` in sync. (Inspired by 9router#1761.) ([#5996](https://github.com/diegosouzapw/OmniRoute/pull/5996)) - **feat(server):** support reverse-proxy `basePath` deployment via a new opt-in `OMNIROUTE_BASE_PATH` env var (empty by default), using Next.js's native `basePath` support so a deployment behind a reverse-proxy subpath (e.g. `https://host/omniroute/`) works without manual header stripping; the two hardcoded auth-redirect targets in `src/server/authz/pipeline.ts` now prefix with `request.nextUrl.basePath`. Default empty basePath is a no-op for existing root-path deployments. (Inspired by 9router#1810.) ([#5992](https://github.com/diegosouzapw/OmniRoute/pull/5992)) - **feat(providers):** add **SumoPod** (`ai.sumopod.com`) and **X5Lab** (`api.x5lab.dev`) OpenAI-compatible BYOK aggregator gateways, wired via the default executor with bearer API-key auth; both use `passthroughModels` with a live `/v1/models` fetcher instead of a hardcoded catalog. Regression guard: `tests/unit/sumopod-x5lab-provider.test.ts`. (Inspired by 9router#1288.) ([#5963](https://github.com/diegosouzapw/OmniRoute/pull/5963)) - **feat(providers):** add **Charm Hyper** (`hyper.charm.land`) as a new OpenAI-compatible, bearer-auth API-key gateway provider with a free tier (100 monthly Hypercredits); models resolve via passthrough (`modelsUrl` + live `/v1/models`) since the catalog isn't publicly documented. (Inspired by 9router#2006.) ([#5961](https://github.com/diegosouzapw/OmniRoute/pull/5961)) - **feat(providers):** add **Nube.sh** (`ai.nube.sh`) as a new BYOK OpenAI-compatible gateway (LiteLLM proxy), Bearer/API-key auth. Its live model catalog is only reachable with a valid key, so no model IDs are hardcoded — it uses `passthroughModels` + `modelsUrl` for live enumeration. (Inspired by 9router#2294.) ([#5936](https://github.com/diegosouzapw/OmniRoute/pull/5936) — thanks @whale9820) - **feat(providers):** add **b.ai** (`api.b.ai`) as a new OpenAI-compatible BYOK provider, distinct from the existing thebai/theb.ai provider, using passthrough model discovery with no hardcoded model list. (Inspired by 9router#963.) ([#5969](https://github.com/diegosouzapw/OmniRoute/pull/5969)) - **feat(providers):** add **Qiniu** (七牛云) AI inference gateway as a BYOK API-key provider — proxies many upstream models (DeepSeek V3/V4, Claude, Kimi, and more) behind a single key, shipping with an empty static seed and relying on `passthroughModels` + the live `/v1/models` catalog instead of a stale hardcoded model id. Regression guard: `tests/unit/qiniu-provider.test.ts`. (Inspired by 9router#911.) ([#5966](https://github.com/diegosouzapw/OmniRoute/pull/5966)) - **feat(providers):** port **ModelScope** (Alibaba 魔搭) as a new API-key, OpenAI-compatible provider — verified against ModelScope's own docs that the real production domain is `api-inference.modelscope.cn` (`.cn`, not the upstream PR's `.ai`) and shipped `passthroughModels: true` with an empty seed + `modelsUrl` instead of the upstream PR's static 5-model snapshot, since the open-model catalog moves fast. (Ported from 9router#1764.) ([#5965](https://github.com/diegosouzapw/OmniRoute/pull/5965) — thanks @tn5052) - **feat(providers):** add **Augment (Auggie CLI)** as a new local, no-auth provider that spawns the user's local `auggie` CLI and pipes a flattened prompt via stdin, wrapping stdout as an OpenAI-compatible SSE stream or single JSON body. Auth is delegated to `auggie login` outside OmniRoute (synthetic `noAuth: true` connection, no DB row required); "Test Connection" spawns `auggie --version`. Hardened against the untrusted-input spawn sink: no `shell: true` on Windows (argv passed straight to the OS loader, no metacharacter interpretation), and `model` is validated against the registry allowlist before spawn (rejecting unknown or `-`-prefixed values) with a trailing `--` end-of-options marker. (Inspired by 9router#1200.) ([#5972](https://github.com/diegosouzapw/OmniRoute/pull/5972) — thanks @chamdanilukman) - **feat(providers):** add **NVIDIA NIM image generation** — a dedicated `nvidia-nim` image format/handler (separate host, `ai.api.nvidia.com/v1/genai/`, native NIM body shape) for the 4 FLUX models (flux.1-dev, flux.1-schnell, flux.1-kontext-dev, flux.2-klein-4b), shaping each model's per-model request body (dimension/mode validation, required input image + aspect ratio, optional edit image) and normalizing the NIM response's varying shapes into the OpenAI `{created, data}` shape. (Inspired by 9router#1195.) ([#5971](https://github.com/diegosouzapw/OmniRoute/pull/5971)) - **feat(oauth):** import a Codex connection from a raw ChatGPT access token — OmniRoute's only Codex import path previously required both `access_token` and `refresh_token`, leaving no path for a user with only a bare ChatGPT website access token. `createProviderConnection` gains an explicit `access_token` auth-type branch (intentionally never deduped), a new `POST /api/oauth/codex/import-token` route (Zod-validated), and `OAuthModal`'s manual-paste path now detects an `eyJ`-prefixed pasted token and posts it to the new endpoint, mirroring the existing grok-cli raw-token flow. The executor's `refreshCredentials()` already degrades safely to `null` without a refresh token, forcing re-auth on expiry. (Inspired by 9router#1290.) ([#5995](https://github.com/diegosouzapw/OmniRoute/pull/5995) — thanks @ryanngit) - **feat(dashboard):** add a tool-source diagnostics settings toggle — a new Settings → Advanced card lets operators flip the existing `logToolSources` flag from the UI instead of editing the DB row directly; `logToolSources` is added to the `.strict()` `/api/settings` Zod PATCH schema (previously rejected). (Inspired by 9router#1825.) ([#5978](https://github.com/diegosouzapw/OmniRoute/pull/5978) — thanks @DuyPrX) - **feat(dashboard):** collapse and sort provider quota rows by remaining percentage — the expanded quota list is sorted highest-remaining-first and collapsed to the first 3 rows by default, with a "Show N more"/"Show less" toggle when a connection reports more than 3 quotas, keeping at-risk quotas visible above a long list of healthy ones. Sort/slice logic extracted into pure, directly-unit-tested helpers (`sortQuotasByRemaining`, `getVisibleQuotas`). (Inspired by 9router#1919.) ([#5977](https://github.com/diegosouzapw/OmniRoute/pull/5977)) - **feat(dashboard):** suggest HuggingFace Hub media models — a new `GET /api/v1/providers/suggested-models` route proxies the public HF Hub models search API (Zod-validated, no token exposed client-side) and `ImageExampleCard` merges the results into the model picker as a selectable chip row for the huggingface provider; also adds a dedicated `huggingface-image` format/handler for HF's raw-image-bytes response. (Inspired by 9router#1633.) ([#5990](https://github.com/diegosouzapw/OmniRoute/pull/5990)) - **feat(cli-tools):** add a **Crush** entry to the dashboard CLI-Tools catalog plus a new `/api/cli-tools/crush-settings` route (GET/POST/DELETE) — OmniRoute already shipped a `crush` CLI setup command (`bin/cli/commands/setup-crush.mjs`) but the dashboard catalog had no matching entry; the new route writes to the same canonical `~/.config/crush/crush.json` path so the dashboard and CLI command agree. (Inspired by 9router#1233.) ([#5970](https://github.com/diegosouzapw/OmniRoute/pull/5970)) - **feat(providers):** extend Vercel AI Gateway (`vercel-ai-gateway`/`vag`) beyond chat-only to support **embeddings and image generation** — the gateway's OpenAI-compatible `/v1` API also exposes `/embeddings` and `/images/generations`, so entries were added to `EMBEDDING_PROVIDERS` (`embeddingRegistry.ts`) and `IMAGE_PROVIDERS` (`imageRegistry.ts`) modeled on the existing `openai` entries. ([#5968](https://github.com/diegosouzapw/OmniRoute/pull/5968) — thanks @tantai-newnol) - **feat(api-keys):** add per-key **device/connection tracking** — a SHA-256 fingerprint of IP + User-Agent, with a 30-minute TTL and per-key/global caps, tracks distinct client devices seen with each API key (in-memory only, raw IP never stored). A new `GET /api/keys/[id]/devices` route exposes masked device details, and the API Keys dashboard tab gets a "Devices" count badge alongside the existing Sessions badge. This is a new granularity distinct from the existing `maxSessions` cap, which limits concurrent sticky-routing sessions rather than tracking device identity. ([#5998](https://github.com/diegosouzapw/OmniRoute/pull/5998) — thanks @mugni-rukita) - **feat(proxy):** add **Webshare** (`proxy.webshare.io`) as a fourth source in the free-proxy provider framework alongside 1proxy, Proxifly, and IPLocate. `WebshareProvider` paginates the account's `/api/v2/proxy/list/` endpoint, upserts proxies into the shared `free_proxies` table, and tombstones proxies the account no longer lists while never touching rows already promoted into the live proxy pool. Unlike the other sources, Webshare is a paid per-account list, gated on `FREE_PROXY_WEBSHARE_API_KEY`. ([#5993](https://github.com/diegosouzapw/OmniRoute/pull/5993) — thanks @ricatix) - **feat(antigravity):** support custom **Google Cloud project ID** settings from the connection edit modal (Antigravity family). ([#5905](https://github.com/diegosouzapw/OmniRoute/pull/5905) — thanks @nickwizard) - **feat(dashboard):** add a **wildcard-CORS runtime warning** banner (Settings → Authorization) when `CORS_ALLOW_ALL`/`*` origins are in effect, plus a new `docs/security/CORS.md` security guide covering the risk and safer alternatives. ([#5602](https://github.com/diegosouzapw/OmniRoute/issues/5602), [#5759](https://github.com/diegosouzapw/OmniRoute/pull/5759)) - **feat(api):** add a `/v1/audio/translations` endpoint (Whisper-style audio translation), a new `audioTranslation` handler, and translation providers wired into `audioRegistry`. Regression guard: `tests/unit/audio-translations-route.test.ts` (8, incl. no-stack-leak). ([#5809](https://github.com/diegosouzapw/OmniRoute/pull/5809)) - **feat(providers):** allow a **custom icon URL** for compatible provider nodes (migration 113 + `nodes.ts` + Zod schema + API routes + catalog + `ProviderIcon` UI). Regression guards: 14 backend + 5 frontend(vitest) + 24 page-utils tests. ([#5815](https://github.com/diegosouzapw/OmniRoute/pull/5815)) - **feat(xai):** register a dedicated `XaiExecutor` with reasoning-effort suffix parsing. Regression guard: `tests/unit/executors/xai-executor.test.ts` (6). ([#5800](https://github.com/diegosouzapw/OmniRoute/pull/5800)) - **feat(webfetch):** support **self-hosted FireCrawl** instances via `FIRECRAWL_BASE_URL`/`FIRECRAWL_TIMEOUT_MS`. Regression guard: `tests/unit/executors/firecrawl-fetch.test.ts` (4). ([#5793](https://github.com/diegosouzapw/OmniRoute/pull/5793)) - **feat(providers):** add **ClinePass** as a first-class API-key (BYOK) provider — Cline's paid gateway (`cline-pass/*` models, plain Bearer key), distinct from the existing OAuth `cline` provider. Regression guard: 16 clinepass tests. ([#5942](https://github.com/diegosouzapw/OmniRoute/pull/5942) — thanks @adentdk) - **feat(relay):** gate **Bifrost auto-routing** by the provider plugin manifest — only manifest-eligible providers reach the sidecar; ineligible/unknown providers fall back to the existing TS routing path with explicit reasons. Regression guards: 4 provider-plugin-manifest + 11 relay-routing-backend tests. ([#5870](https://github.com/diegosouzapw/OmniRoute/pull/5870) — thanks @KooshaPari) - **feat(providers):** wire **Claude Sonnet 5** end-to-end across the model pipeline — registries, `modelSpecs`, pricing (×3), cost, Sonnet-family fallback, 1M-context, and static models. ([#5833](https://github.com/diegosouzapw/OmniRoute/pull/5833) — thanks @ggiak) ### 🔧 Bug Fixes - **dashboard (`/dashboard/system/proxy` 500 on every render):** `ProxyRegistryManager` called `useProxyBatchOperations(load)` before the `const load = useCallback(...)` declaration in the component body, so every server render threw a TDZ `ReferenceError: Cannot access 'load' before initialization` and the whole proxy page 500'd (#5918 regression, caught by the release-PR e2e smoke — the PR→release fast-gates never render pages). The hook block now sits after the `load` declaration. Regression guard: `tests/unit/ui/ProxyRegistryManager-tdz-render.test.tsx` (SSR renderToString — the exact crash mode). - **server (TRACE/TRACK/CONNECT returned raw 500 on every route):** methods that undici/fetch cannot represent blew up inside Next's middleware adapter (`TypeError: 'TRACE' HTTP method is unsupported.`) as an unhandled 500 (caught by the release-PR dast-smoke Schemathesis negative tests on the new `/api/keys/{id}/devices` endpoint). The raw HTTP method guard now answers a clean 405 + `Allow` header for these methods on any path, before Next sees the request. Regression guard: `tests/unit/dast-method-not-allowed.test.ts` (new case). - **i18n (auto-detect refreshed every first visit):** `LocaleAutoDetect` (#5979) called `router.refresh()` on every cookie-less first visit — even when the detected browser locale was exactly the one the server had just rendered — re-navigating the page mid-interaction (flaky e2e "execution context destroyed" + a visible flash for every new visitor). It now refreshes only when the detected locale differs from the server-rendered ``. Regression guard: `tests/unit/ui/LocaleAutoDetect-refresh.test.tsx`. - **models (`oc/` alias must reach the no-auth OpenCode provider):** restore the [#2901](https://github.com/diegosouzapw/OmniRoute/issues/2901) routing contract after the #5918 transitive-alias change made the registered no-auth `opencode` provider unreachable by any prefix (`oc/` chained through the manual `opencode` → `opencode-zen` slug override and misrouted its combo entries). `resolveProviderAlias` now stops the alias chain as soon as a hop lands on a registered provider id, while keeping #5918's transitivity across alias-only hops and its loop/depth guards. Regression guards: `tests/unit/combo-builder-opencode-prefix.test.ts`, `tests/unit/provider-alias-transitive-5918.test.ts`. - **providers (Auggie executor EPIPE crash):** a fast-exiting `auggie` CLI (e.g. binary present but immediately failing) delivered `EPIPE` **asynchronously** as an `'error'` event on the child's stdin stream — which a plain try/catch around `stdin.write()` cannot catch — crashing the request instead of surfacing the sanitized CLI error. Both spawn sites now attach a stdin `'error'` handler so the child's own exit/close handlers report the failure. Regression guard: `tests/unit/auggie-executor.test.ts` (deterministic 3/3 locally). - **dashboard (CoolingConnectionsPanel broke `next build`):** the cooling-connections panel from #6061 imported `Card` from a shadcn-style path that does not exist in this repo (`@/components/ui/card`) and pulled the server DB barrel (`@/lib/localDb`) into a client component — `next build` failed to compile on the release branch. The panel now renders with repo-native markup and reads `formatResetCountdown` from the new client-safe `src/shared/utils/formatting.ts`. Regression guards: `tests/unit/format-reset-countdown.test.ts`, `tests/unit/ui/CoolingConnectionsPanel.test.tsx`. ([#6155](https://github.com/diegosouzapw/OmniRoute/pull/6155)) - **oauth (Zed "Unknown provider" crash):** adding **Zed** from the providers dashboard threw an unhandled `OAuth GET error: Unknown provider: zed` (500) ([#6041](https://github.com/diegosouzapw/OmniRoute/issues/6041)). Zed is a **keychain-import-only** provider — it's listed in the OAuth catalog so the UI shows it, but has no OAuth handler, so the generic `/api/oauth/[provider]/[action]` route hit `getProvider("zed")` and crashed. The route now recognizes keychain-import-only providers and returns a clear **400** pointing users at the **Import** button (for both GET and POST OAuth actions), instead of a 500. Regression guard: `tests/unit/oauth-keychain-import-only-6041.test.ts`. (thanks @imblowsnow) - **fix(providers):** disable the unsupported `thinking` param for `minimax-m2.7` on NVIDIA NIM (the upstream rejects it) ([#6102](https://github.com/diegosouzapw/OmniRoute/pull/6102)). Regression guard: `tests/unit/nvidia-minimax-thinking-strip.test.ts`. (thanks @anki1kr) - **fix(mitm):** add an in-process guard so concurrent MITM server starts no longer race — a second start while one is already in flight is short-circuited instead of double-binding the listener ([#6107](https://github.com/diegosouzapw/OmniRoute/pull/6107)). Regression guard: `tests/unit/mitm-start-guard.test.ts`. (thanks @anki1kr) - **translator (Responses → Chat Completions):** strip the Responses-API-only `truncation` field before forwarding a `/v1/responses` request to a non-OpenAI Chat Completions upstream ([#6109](https://github.com/diegosouzapw/OmniRoute/pull/6109)). Strict upstreams (e.g. NVIDIA NIM) rejected it with HTTP 400 `Unsupported parameter(s): truncation`, breaking Codex-style clients routed to those providers. `client_metadata`, `background`, and `safety_identifier` were already stripped — `truncation` was the remaining gap. Regression guard: `tests/unit/responses-strip-truncation-2311.test.ts`. (thanks @TuanNguyen0708) - **combo (prefer known context capacity over unknown):** when a combo filters out at least one target for exceeding a _known_ context limit, the router now prefers the remaining known-compatible targets over targets whose context metadata is simply unknown, instead of letting unknown-metadata targets be the only survivors. If no known-compatible context target remains, context-only candidates fall back to the normal strategy order. Regression guard: `tests/unit/combo-context-window-filter.test.ts`. ([#6088](https://github.com/diegosouzapw/OmniRoute/pull/6088) — thanks @Thinkscape) - **models (GLM-5.2 context normalization):** stop treating every hosted GLM-5.2 provider alias as the native 1M-context model. Native/bare GLM-5.2 and verified OpenCode / ZenMux routes keep their 1,000,000-token context, while hosted-provider aliases now respect the caps declared in their provider metadata instead of inheriting the native max. Regression guards: `tests/unit/model-capabilities-registry.test.ts`, `tests/unit/models-catalog-route.test.ts`. ([#6091](https://github.com/diegosouzapw/OmniRoute/pull/6091) — thanks @Thinkscape) - **providers (Gemini Web):** refresh the Gemini Web cookie handling and model catalog so live Gemini Web sessions keep authenticating and routing to current models. Regression guard: `tests/unit/gemini-web.test.ts`. ([#6095](https://github.com/diegosouzapw/OmniRoute/pull/6095) — thanks @backryun) - **providers (Perplexity Web):** refresh the Perplexity Web model catalog to the current set (GPT-5.4/5.5, Claude Sonnet 5.0 / Opus 4.8, GLM-5.2, Kimi K2.6, Nemotron 3 Ultra) and update the internal mode / `model_preference` mappings and thinking variants so requests resolve to live upstream models. Regression guard: `tests/unit/perplexity-web.test.ts`. ([#6106](https://github.com/diegosouzapw/OmniRoute/pull/6106) — thanks @backryun) - **dashboard ("Update now" → Internal Server Error):** clicking **Update now** on the dashboard home could crash the page with a blank "Internal Server Error" screen (`Minified React error #31`). The handler POSTs the loopback-only `/api/system/version` auto-update endpoint and, on a non-OK JSON response (e.g. a `403` when the dashboard is reached through a reverse proxy / non-loopback origin), passed the raw error envelope object `{ error: { code, message, correlation_id } }` straight to `notify.error()`, which rendered the object as a React child and threw #31. The update-error path now funnels the body through `extractApiErrorMessage()` (the same safe extractor added in #5340), so a readable string always reaches the toast. Regression guard: `tests/unit/ui/home-update-error-render-5991.test.ts`. ([#5991](https://github.com/diegosouzapw/OmniRoute/issues/5991)) - **fix(onboarding):** route the provider-details link in the onboarding wizard by the node's stable id instead of the composite provider slug, which could point at the wrong provider details page for multi-account/fingerprint nodes. Regression guard: `tests/unit/onboarding-wizard-details-link-6145.test.ts`. ([#6145](https://github.com/diegosouzapw/OmniRoute/pull/6145) — thanks @chirag127) - **fix(cli):** give `setup-claude` a fallback profile generator mirroring `setup-codex`, so profile generation no longer silently no-ops when the primary generator path is unavailable. Regression guard: `tests/unit/cli/setup-claude.test.ts` (new cases). ([#6138](https://github.com/diegosouzapw/OmniRoute/pull/6138) — thanks @derhornspieler) - **fix(glm):** suppress a leaked `` close marker in the GLM Anthropic transport, which was surfacing the raw reasoning-close tag in visible response content instead of being consumed as part of the thinking-block framing. Regression guard: `tests/unit/glm-think-close-marker-leak.test.ts`. ([#6133](https://github.com/diegosouzapw/OmniRoute/pull/6133) — thanks @dhaern) - **fix(provider-limits):** close a TOCTOU race in quota-recovery clearing by moving the check-then-clear to a CAS (compare-and-swap) primitive in `src/lib/db/providers.ts`, so two concurrent recovery paths can no longer both observe stale state and double-clear/re-lock a connection. Regression guard: `tests/unit/provider-limits-recovery.test.ts`. ([#6139](https://github.com/diegosouzapw/OmniRoute/pull/6139) — thanks @janeza2) - **fix(provider-limits):** clear transient rate-limit state (`rateLimitedUntil`, `lastError`, `backoffLevel`) as soon as quota recovers, instead of leaving stale rate-limit fields behind that could keep a now-healthy connection looking unavailable. Regression guard: `tests/unit/provider-limits-recovery.test.ts`. ([#6128](https://github.com/diegosouzapw/OmniRoute/pull/6128) — thanks @janeza2) - **combos (OpenCode/MiMo fingerprint accounts):** expand fingerprint-scoped OpenCode/MiMo accounts into their full per-fingerprint set in the combo builder, which previously showed only the first matching account entry and hid the rest from combo target selection. Regression guard: `tests/unit/combo-builder-fingerprint-expansion.test.ts`. ([#6092](https://github.com/diegosouzapw/OmniRoute/pull/6092), closes [#6087](https://github.com/diegosouzapw/OmniRoute/issues/6087) — thanks @anki1kr) - **fix(auth):** persist quota-preflight account lockouts until the reset window elapses, instead of losing the lockout on process restart and letting a still-quota-exhausted account be selected again immediately. Regression guards: `tests/unit/sse-auth.test.ts`, `tests/unit/opencode-quota-fetcher.test.ts`, `tests/unit/usage-service-hardening.test.ts`. ([#6090](https://github.com/diegosouzapw/OmniRoute/pull/6090) — thanks @Thinkscape) - **combo (fingerprint-based provider expansion):** expand fingerprint-based providers into per-fingerprint combo targets (`open-sse/services/combo/fingerprintExpansion.ts`) so a combo referencing a fingerprint-scoped provider fans out to every matching fingerprint account instead of collapsing onto one. Regression guards: `tests/unit/combo-fingerprint-expansion.test.ts`, `tests/integration/fingerprint-expansion.test.ts`. ([#6082](https://github.com/diegosouzapw/OmniRoute/pull/6082) — thanks @pizzav-xyz) - **fix (safety-net redirect `reqId` crash):** fix a `reqId` `ReferenceError` thrown inside the safety-net combo redirect path in `src/sse/handlers/chat.ts`, remove dead code in `src/domain/quotaCache.ts`, and rename the stray root `DESING.md` to `DESIGN.md`. Regression guard: `tests/unit/chat-safetynet-reqid-6097.test.ts`. ([#6097](https://github.com/diegosouzapw/OmniRoute/pull/6097) — thanks @fix2015) - **fix(compression):** send a patch-only body to `PUT /api/settings/compression` from `CompressionHub`, instead of round-tripping the full settings object and risking clobbering fields changed elsewhere between load and save. Regression guard: `tests/unit/ui/CompressionHub-patch-only.test.tsx`. ([#6077](https://github.com/diegosouzapw/OmniRoute/pull/6077), closes [#6039](https://github.com/diegosouzapw/OmniRoute/issues/6039) — thanks @anki1kr) - **fix(codex):** use `access_token.exp` instead of `id_token.exp` when computing `expiresAt` on Codex auth import, since the `id_token` can expire far sooner than the actual access token, causing imported connections to be treated as expired while still usable. Regression guard: `tests/unit/codex-auth-import-expiry.test.ts`. ([#6084](https://github.com/diegosouzapw/OmniRoute/pull/6084), closes [#6075](https://github.com/diegosouzapw/OmniRoute/issues/6075) — thanks @anki1kr) - **fix(security):** persist the IP allow/block-list configuration (it was resetting to Disabled and clearing configured IPs on every restart/update) and actually enforce it in the authz pipeline (`src/server/authz/pipeline.ts`), where it was previously validated but never applied. Regression guards: `tests/unit/ip-filter-persistence-6131.test.ts`, `tests/unit/authz/ip-filter-enforcement-6131.test.ts`, `tests/unit/ip-filter.test.ts`. (closes [#6131](https://github.com/diegosouzapw/OmniRoute/issues/6131), [#6132](https://github.com/diegosouzapw/OmniRoute/pull/6132)) - **fix (Claude tool_result adjacency):** reattach an OpenAI-shaped `tool_result` to sit directly adjacent to its originating `tool_use` before translating to Claude's message format (`open-sse/translator/request/openai-to-claude/toolResultAdjacency.ts`), since Claude's API rejects/mishandles a tool result separated from its tool call by intervening messages. Regression guard: `tests/unit/translator-openai-to-claude.test.ts` (new cases). ([#6035](https://github.com/diegosouzapw/OmniRoute/pull/6035) — thanks @KooshaPari) - **fix(config):** externalize `ws`/`bufferutil`/`utf-8-validate` in `next.config.mjs` so the `copilot-m365-web` executor's WebSocket masking path works at runtime — chat requests through it were silently timing out because the bundler was inlining `ws` instead of leaving it as a real Node dependency. Regression guard: `tests/unit/next-config.test.ts`. ([#6130](https://github.com/diegosouzapw/OmniRoute/pull/6130), closes [#6062](https://github.com/diegosouzapw/OmniRoute/issues/6062) — thanks @anki1kr, whose #6098 fix it re-lands) - **fix(registry):** update grok-cli model context lengths to match the actual Grok CLI `/context` capacities — `grok-build` 128k→256k, `grok-composer-2.5-fast` 128k→200k — so context-aware routing stops filtering these models out for exceeding a stale, too-low limit. Registry-only. ([#5913](https://github.com/diegosouzapw/OmniRoute/pull/5913) — thanks @Chewji9875) - **fix(providers):** strip an orphan `tool_result` (one with no preceding `tool_use`) on the Antigravity MITM path before translating to OpenAI format, since an unpaired tool result upstream caused request failures. Regression guard: `tests/unit/antigravity-orphan-toolresult-6026.test.ts`. (closes [#6026](https://github.com/diegosouzapw/OmniRoute/issues/6026), [#6115](https://github.com/diegosouzapw/OmniRoute/pull/6115)) - **fix(providers):** emulate OpenAI-style `tool_calls` in the GitLab Duo executor (new `open-sse/executors/gitlabResponses.ts`), since the executor previously didn't emulate tool-call semantics for Duo, breaking tool-using clients routed to GitLab Duo. Regression guard: `tests/unit/gitlab-duo-toolcalls-6051.test.ts`. (closes [#6051](https://github.com/diegosouzapw/OmniRoute/issues/6051), [#6111](https://github.com/diegosouzapw/OmniRoute/pull/6111)) - **fix(429 / accountFallback):** persist the per-account 429 cooldown cascade across the request boundary and classify OpenCode's "Monthly usage limit. Resets in N days." message as a connection-scoped quota exhaustion with an N-day cooldown (instead of a ~5s transient retry), so an exhausted account stops being re-selected until its window resets. ([#6061](https://github.com/diegosouzapw/OmniRoute/pull/6061) — thanks @KooshaPari / @anki1kr, whose superseded #6086 carried the same day-parser approach) - **combo (sibling-model fallback on per-model-quota 500s):** when a combo held multiple models from the same provider (e.g. two Gemini models) and the first returned a server 500, the router retried the same locked model and surfaced a 429 "cooling down" instead of trying the sibling — `markConnectionLevelExhaustion` was wrongly tripped by a model-level 500 for per-model-quota providers (gemini, github, passthrough, compatible), and the retry loop didn't check `isModelLocked` before re-hitting the same model. Both gaps are fixed; the combo now falls through to the untried sibling model. Regression guard: `tests/unit/combo/combo-target-exhaustion.test.ts` (21 cases). ([#5976](https://github.com/diegosouzapw/OmniRoute/pull/5976) — thanks @hartmark) - **providers (Cline non-streaming envelope):** Cline can return OpenAI-compatible chat completions wrapped as `{ success, data: { choices, usage, ... } }`; the non-streaming path checked the top-level body for empty content before unwrapping, so a valid wrapped response could be misclassified as malformed/empty. The envelope is now unwrapped immediately after provider-envelope handling, before empty-content detection, usage extraction, and translation. Regression guard: `tests/unit/cline-response-envelope.test.ts`. ([#6046](https://github.com/diegosouzapw/OmniRoute/pull/6046) — thanks @KooshaPari) - **providers (kimi-web, qwen-web):** align the kimi-web model catalog and request-scenario selection with `www.kimi.com`'s live `GetAvailableModels` response, and stop aliasing `qwen3-coder-plus` on qwen-web now that it is present as its own model in the live Qwen web catalog. ([#5915](https://github.com/diegosouzapw/OmniRoute/pull/5915) — thanks @janeza2) - **translator (Antigravity/Gemini tool schemas):** strip `multipleOf` from function-declaration parameters before forwarding to Antigravity/Gemini — it is not part of the Gemini OpenAPI 3.0 schema subset accepted upstream and triggered a hard 400 ("Unknown name multipleOf"). Added to `GEMINI_UNSUPPORTED_SCHEMA_KEYS` so it is stripped at every schema level; `minimum`/`maximum` are unaffected since Gemini accepts them. (Ported from 9router#2309, reported by @abil0321.) ([#6052](https://github.com/diegosouzapw/OmniRoute/pull/6052)) - **translator (Kiro system prompt leak):** Kiro/CodeWhisperer has no system role, so system messages were normalized into a bare user turn — the full Claude Code system prompt then appeared as raw user text, polluting model context. System-origin content is now wrapped in `` tags before merging into the Kiro user message; real user turns are unaffected. (Ported from 9router#2306, reported by @VitzS7.) ([#6053](https://github.com/diegosouzapw/OmniRoute/pull/6053)) - **fix(codex):** convert Chat Completions `json_schema` `response_format` → Responses API `text.format` on the Codex path, and preserve an existing `text.format` through verbosity normalization. Regression guards: 48 translator-openai-responses-req + 8 codex-verbosity tests. ([#5933](https://github.com/diegosouzapw/OmniRoute/pull/5933) — thanks @yusufrahadika) - **fix(thinking):** only inject the `redacted_thinking` replay block when `tool_use` is present and thinking is enabled, avoiding a fabricated replay block on plain (non-tool) turns. ([#5945](https://github.com/diegosouzapw/OmniRoute/issues/5945), [#5953](https://github.com/diegosouzapw/OmniRoute/pull/5953)) - **fix(resilience):** honor active **codex session affinity** over per-request reset-aware re-scoring, so an in-flight session sticks to its pinned account instead of being re-scored away mid-conversation. New `src/sse/services/sessionAffinityPin.ts` module. Regression guard: `tests/unit/codex-session-affinity-reset-aware-5903.test.ts`. ([#5903](https://github.com/diegosouzapw/OmniRoute/issues/5903), [#5943](https://github.com/diegosouzapw/OmniRoute/pull/5943)) - **fix(resilience):** compute per-window `is_exhausted` and honor the quota-exhaustion preflight for **priority combos**, so a combo no longer keeps routing to a target whose current window is already exhausted. New `open-sse/services/combo/quotaExhaustionCutoff.ts`. Regression guard: `tests/unit/combo-priority-quota-exhaustion-cutoff-5923.test.ts`. ([#5923](https://github.com/diegosouzapw/OmniRoute/issues/5923), [#5941](https://github.com/diegosouzapw/OmniRoute/pull/5941)) - **fix(providers):** strip a `/v1` suffix from the base URL unconditionally in both models-discovery paths, avoiding a doubled `/v1/v1/models` fetch error (e.g. Api Airforce). Regression guard: `tests/unit/airforce-v1-double-prefix-5899.test.ts`. ([#5899](https://github.com/diegosouzapw/OmniRoute/issues/5899), [#5920](https://github.com/diegosouzapw/OmniRoute/pull/5920) — thanks @anki1kr) - **fix(api):** relax provider-scoped chat completion validation on `/api/providers/[provider]/chat/completions`. Regression guard: `tests/unit/provider-scoped-chat-completions-validation.test.ts`. ([#5907](https://github.com/diegosouzapw/OmniRoute/pull/5907) — thanks @nickwizard) - **fix(providers):** validate **v0 Platform** (Vercel) API keys via the `/chats` endpoint instead of a probe that rejected valid keys. Regression guard: `tests/unit/provider-validation-specialty.test.ts`. ([#5954](https://github.com/diegosouzapw/OmniRoute/pull/5954) — thanks @vittoroliveira-dev) - **fix(mcp):** auto-recover stale streamable HTTP MCP sessions on `initialize` instead of failing the reconnect. Regression guard: `tests/unit/mcp-session-sweep.test.ts`. ([#5957](https://github.com/diegosouzapw/OmniRoute/pull/5957) — thanks @Chewji9875) - **fix(translator):** enforce strict Anthropic content-block compliance when converting an antigravity → openai request. Regression guard: `tests/unit/translator-antigravity-to-openai.test.ts` (9). ([#5935](https://github.com/diegosouzapw/OmniRoute/pull/5935)) - **fix(sse):** strip ANSI/VT100 escape codes from `gemini-cli` stream frames using a ReDoS-safe pattern. Regression guard: `tests/unit/gemini-cli-ansi-sanitization.test.ts` (5). ([#5934](https://github.com/diegosouzapw/OmniRoute/pull/5934) — thanks @anki1kr) - **fix(discovery):** resolve a doubled `/v1` discovery path and a `REDIRECT_BLOCKED` probe-loop abort in the model-discovery route. Regression guard: `tests/unit/provider-models-route.test.ts`. ([#5904](https://github.com/diegosouzapw/OmniRoute/pull/5904) — thanks @hamsa0x7) - **fix(providers): Perplexity Web now emits real `tool_calls` in streaming mode** — previously only non-streaming requests (`hasTools && !stream`) converted `{...}` text into OpenAI `tool_calls`; streaming requests (the default for agentic coding clients) got the raw `` text as plain `delta.content` and never emitted a `tool_calls` SSE delta. Now mirrors the `chatgpt-web` `toolMode` helpers (`buildToolModeResponse()`/`toolCompletionToSseStream()`, extended with a caller-supplied `idSeed` so tool-call ids stay provider-specific), buffering the completion and emitting a terminal SSE replay carrying `delta.tool_calls` + `finish_reason: tool_calls` regardless of the caller's stream flag. ([#5927](https://github.com/diegosouzapw/OmniRoute/issues/5927), [#5937](https://github.com/diegosouzapw/OmniRoute/pull/5937)) - **providers (openai-family model inference no longer hijacks cataloged models):** `resolveModelByProviderInference()` had an unconditional `/^gpt-/i` heuristic that hijacked any model id starting with `gpt-`/`o1`/`o3` into provider `openai`, even when the id is cataloged under other providers — breaking bare (non-combo) requests for open-weight models like `gpt-oss-120b` (served by fireworks/cerebras/scaleway/byteplus/sambanova/heroku), which don't exist on openai's catalog, producing a 404 with no fallback. The heuristic is now gated on `providers.length === 0` so it only fires for genuinely uncataloged openai-family ids. Regression guard: `tests/unit/gptoss-provider-inference-5852.test.ts`. ([#5852](https://github.com/diegosouzapw/OmniRoute/issues/5852), [#5938](https://github.com/diegosouzapw/OmniRoute/pull/5938)) - **fix(providers): deepseek-web reliability** — auto-refresh the session on `401`/`403`, refresh the v2.0.0 client headers, and fix the token-kind bulk import path. Regression guards: `tests/unit/deepseek-web-autorefresh-401-response.test.ts`, `tests/unit/bulk-web-session-import.test.ts`. ([#5988](https://github.com/diegosouzapw/OmniRoute/pull/5988) — thanks @backryun) - **fix(api):** guard the shared frontend API client (`handleResponse` in `src/shared/utils/api.ts`) against non-JSON error responses — it previously called `response.json()` unconditionally and read `data.error` directly, throwing an unrelated parse error (or `undefined`) instead of a useful message when an upstream/proxy returned a non-JSON error body. Now routes through `parseResponseBody`/`getErrorMessage` to build a safe message regardless of body shape. Regression guard: `tests/unit/shared-api-utils.test.ts`. ([#5973](https://github.com/diegosouzapw/OmniRoute/pull/5973)) - **fix(embeddings):** forward the connection-level proxy configuration to embedding requests — `src/lib/embeddings/service.ts` previously ignored a connection's configured proxy when making embedding calls, so proxy-only network setups leaked embedding traffic outside the proxy. Regression guard: `tests/unit/embeddings-proxy-forwarding.test.ts`. ([#5975](https://github.com/diegosouzapw/OmniRoute/pull/5975)) - **fix(resilience):** parse `Retry-After` from a 429's JSON body for cooldown calculation, not just the HTTP header — a new `retryAfterJson.ts` helper extracts a retry-after hint from common JSON error-body shapes and `accountFallback.ts`'s cooldown path now prefers it when the header is absent. Regression guard: `tests/unit/account-fallback-retry-after-json.test.ts`. (Includes #6013's retry-after-json extraction.) ([#5974](https://github.com/diegosouzapw/OmniRoute/pull/5974) — thanks @KooshaPari) ### 📝 Maintenance - **release close (release-PR one-pass CI sweep):** restore Zod validation on the provider-scoped chat route with a `.passthrough()` schema that keeps #5907's relaxed semantics (t06 route-validation gate); point `/api/keys/{id}/devices`' 401 response at the management error envelope in `docs/openapi.yaml` (Schemathesis schema-conformance); rebaseline `i18nUiCoverage.pct` 77.5→76.8 (~1352 new en.json UI keys from the cycle await the async translation workflow — same shape as the v3.8.39 rebaseline); dismiss 2 CodeQL `js/incomplete-url-substring-sanitization` false positives on unit-test asserts (v3.8.35 precedent). - **release close (Phase 0 pre-flight):** align cycle-stale tests with merged behavior — provider count 166→167 (Kenari #6104), Linux-regenerated translate-path golden (+`kenari`), OpenCode quota scope `provider`→`connection` (#6061) — and absorb cycle ratchet drift (file-size caps for `oauth/[provider]/[action]/route.ts` 960, `providerLimits.ts` 998, `chat.ts` 1662, `auth.ts` 2426, with #6158 tracked to restore the oauth-route freeze). The test-masking gate gains a narrowly-scoped `_deletedWithReplacement` allowlist section (deletion is exempt ONLY when the declared replacement test file exists in HEAD — used for `targetExhaustion.test.ts` → `tests/unit/combo/combo-target-exhaustion.test.ts`, which has MORE coverage: 21 cases/52 asserts vs 13/37), plus 5 new gate unit tests and reduction-allowlist entries for the verified-legitimate #5958/#6088/#5816 assert migrations. - **test (deflake `setup-claude`):** `tests/unit/cli/setup-claude.test.ts` failed ~50% of runs with `Unable to deserialize cloned data due to invalid or unsupported version` at file teardown (all subtests passed), randomly reddening `Unit Tests fast-path (2/2)` / `Fast Quality Gates` across the PR→release queue. Root cause: `node --test` streams each file's report to the parent as V8-serialized frames on fd 1 (stdout), and the CLI helper under test (`syncClaudeProfilesFromModels`) prints progress via `console.log` — that stdout output interleaved with the serialized frames and corrupted the stream. The test now silences the stdout-writing `console` methods for the file's duration (no assertion inspects stdout), making it deterministic (15/15 green locally). ([#5959](https://github.com/diegosouzapw/OmniRoute/issues/5959)) ([#6021](https://github.com/diegosouzapw/OmniRoute/pull/6021)) - **API validation:** add a `validatedJsonBody(request, schema)` helper in `src/shared/validation/helpers.ts` that fuses JSON body parsing and Zod validation into a single call, returning either the type-narrowed data or a ready-to-return 400 `NextResponse` with the standard error envelope. Salvaged from the closed refactor PR #5075 (Tier 1 portable helper) with a focused 6-case regression test. Co-authored-by: KooshaPari - **repo (Windows case-conflict cleanup):** remove the stale root `DESIGN.md`, which case-conflicted with `design.md` and broke checkouts/clones on case-insensitive Windows filesystems. ([#6140](https://github.com/diegosouzapw/OmniRoute/pull/6140) — thanks @backryun) - **i18n(zh-CN):** translate the CHANGELOG entries and section headings, adopting zh-CN as a fully translated locale alongside the existing supporting docs. ([#6043](https://github.com/diegosouzapw/OmniRoute/pull/6043) — thanks @studyzy) - **docs (env-doc-sync base-red):** document `BIFROST_PORT` in `.env.example` / `docs/reference/ENVIRONMENT.md` — the Bifrost embedded-service merge referenced `process.env.BIFROST_PORT` (default 8080) without documenting it, so `check:env-doc-sync` failed on the release tip and reddened Fast Quality Gates for every open PR→release. Docs-only (`8d7e3e28f`). - **test (CI-runner-independent translate-path golden):** normalize OS/arch-derived request headers (`X-Stainless-Os`/`X-Stainless-Arch`, `(OS;arch)` User-Agent segments, and Antigravity's `os.platform()`-derived platform substring) in the provider translate-path golden snapshot, so the test no longer depends on the OS/arch of the CI runner that generated it — a Mac-literal Antigravity UA was failing on Linux CI. Regression guard: `tests/unit/provider-translate-path-golden.test.ts`. ([#6076](https://github.com/diegosouzapw/OmniRoute/pull/6076) — thanks @KooshaPari) - **release-green base-reds (#5695 regex + file-size rebaseline):** `tests/unit/ui/quick-start-api-keys-link-5695.test.ts` now tolerates Prettier splitting a multi-line `` so the `step1Desc` regex matches the `/dashboard/api-manager` link instead of skipping to `step2`'s single-line `/dashboard/providers` link (test was brittle, not the code). Also rebaselines 5 files that grew via already-merged release-tip PRs in `config/quality/file-size-baseline.json` (`ApiManagerPageClient` 3017→3058, `OAuthModal` 969→989, `cliRuntime` 1090→1100, `webProvidersA` 805→809, `deepseek-web.test` 1081→1092), with shrink tracked in #3501. ([#6093](https://github.com/diegosouzapw/OmniRoute/pull/6093)) - **release close (LEDGER-4 base-red):** the `cline-pass` provider's `minimax-m3` registry entry was missing `supportsVision`, breaking the LEDGER-4 registry-consistency test (every `minimax-m3` entry must set `supportsVision` to match `lite.ts` — the model is multimodal). Flagged it to match every other `minimax-m3` entry (trae, bazaarlink, cline, ollama-cloud, ...). ([#6003](https://github.com/diegosouzapw/OmniRoute/pull/6003)) - **release close (stryker `tap.testFiles` drift):** additional release-green cleanup clearing the `qoder` registry's `minimax-m3` `supportsVision` LEDGER-4 base-red and `stryker.conf.json`'s `tap.testFiles` drift. ([#6012](https://github.com/diegosouzapw/OmniRoute/pull/6012)) - **install (pnpm 11+ support):** pnpm 11 introduced `ERR_PNPM_IGNORED_BUILDS` for native addon packages — without explicit `allowBuilds` approval, packages silently skip their build scripts and OmniRoute fails to start with missing native modules. Sets `allowBuilds=true` for all 13 native addon packages in `pnpm-workspace.yaml` (`@parcel/watcher`, `@swc/core`, `better-sqlite3`, `core-js`, `esbuild`, `keytar`, `koffi`, `libxmljs2`, `onnxruntime-node`, `protobufjs`, `sharp`, `tls-client-node`, `unrs-resolver`) and migrates `onlyBuiltDependencies` from the deprecated `package.json` field to a new `pnpm.json`. (commit 39349da18 — thanks @chirag127) - **refactor (Block J hot-path decomposition):** extract pure leaves with no behavior change from the executor, translator, combo, and SSE hot paths — orphaned executor tests moved to top-level so a runner collects them, and `handleComboChat`'s auto-strategy/target-timeout regions split into named helpers. ([#6063](https://github.com/diegosouzapw/OmniRoute/pull/6063), [#6049](https://github.com/diegosouzapw/OmniRoute/pull/6049), [#6036](https://github.com/diegosouzapw/OmniRoute/pull/6036), [#6030](https://github.com/diegosouzapw/OmniRoute/pull/6030), [#6020](https://github.com/diegosouzapw/OmniRoute/pull/6020), [#6018](https://github.com/diegosouzapw/OmniRoute/pull/6018), [#6017](https://github.com/diegosouzapw/OmniRoute/pull/6017), [#6016](https://github.com/diegosouzapw/OmniRoute/pull/6016), [#6015](https://github.com/diegosouzapw/OmniRoute/pull/6015), [#6014](https://github.com/diegosouzapw/OmniRoute/pull/6014), [#6008](https://github.com/diegosouzapw/OmniRoute/pull/6008), [#6006](https://github.com/diegosouzapw/OmniRoute/pull/6006), [#6000](https://github.com/diegosouzapw/OmniRoute/pull/6000), [#5999](https://github.com/diegosouzapw/OmniRoute/pull/5999), [#5994](https://github.com/diegosouzapw/OmniRoute/pull/5994), [#5967](https://github.com/diegosouzapw/OmniRoute/pull/5967), [#5962](https://github.com/diegosouzapw/OmniRoute/pull/5962), [#5960](https://github.com/diegosouzapw/OmniRoute/pull/5960), [#5947](https://github.com/diegosouzapw/OmniRoute/pull/5947), [#5949](https://github.com/diegosouzapw/OmniRoute/pull/5949), [#5940](https://github.com/diegosouzapw/OmniRoute/pull/5940), [#5932](https://github.com/diegosouzapw/OmniRoute/pull/5932)) - **chore (quality/CI housekeeping):** rebaseline residual ESLint/cognitive-complexity/file-size drift accumulated over the v3.8.44 cycle, move orphaned executor tests to a top-level location so a runner actually collects them, harden the release pipeline with a test-masking pre-flight gate plus contributors/uncovered helpers, and make the `pr-evidence` FAIL output tell the author to push (a body edit alone does not re-run the gate). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926), [#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944), [#5952](https://github.com/diegosouzapw/OmniRoute/pull/5952), [#6027](https://github.com/diegosouzapw/OmniRoute/pull/6027), [#5928](https://github.com/diegosouzapw/OmniRoute/pull/5928), plus a #5975-collateral test hardening pinning a seeded connection to direct egress in route-edge-coverage) - **docs (housekeeping):** normalize mixed-language documentation content, restore the OpenAPI coverage ratchet by documenting 9 newly-added routes, record Hard Rule #22 (cross-session safety — `git stash` + in-flight PR bans), and document the compression-engine's upstream sync policy for the RTK/Caveman engines. ([#6105](https://github.com/diegosouzapw/OmniRoute/pull/6105), [#5955](https://github.com/diegosouzapw/OmniRoute/pull/5955), [#5948](https://github.com/diegosouzapw/OmniRoute/pull/5948), plus docs-only commit 926b08aa8) ### 🙌 Contributors Thanks to everyone whose work landed in v3.8.44: | Contributor | PRs / Issues | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | | [@adentdk](https://github.com/adentdk) | #5942 | | [@anki1kr](https://github.com/anki1kr) | #5899, #5920, #5934, #6039, #6061, #6062, #6075, #6077, #6084, #6086, #6087, #6092, #6098, #6130 | | [@artickc](https://github.com/artickc) | #6119 | | [@backryun](https://github.com/backryun) | #5988, #6095, #6106, #6140 | | [@chamdanilukman](https://github.com/chamdanilukman) | #5972 | | [@Chewji9875](https://github.com/Chewji9875) | #5913, #5957 | | [@chirag127](https://github.com/chirag127) | #6145 | | [@derhornspieler](https://github.com/derhornspieler) | #6138 | | [@dhaern](https://github.com/dhaern) | #6133 | | [@doedja](https://github.com/doedja) | direct commit / report | | [@DuyPrX](https://github.com/DuyPrX) | #5978 | | [@fix2015](https://github.com/fix2015) | #6097 | | [@ggiak](https://github.com/ggiak) | #5833 | | [@hamsa0x7](https://github.com/hamsa0x7) | #5904 | | [@hartmark](https://github.com/hartmark) | #5976 | | [@imblowsnow](https://github.com/imblowsnow) | direct commit / report | | [@janeza2](https://github.com/janeza2) | #5915, #6128, #6139 | | [@KooshaPari](https://github.com/KooshaPari) | #5870, #5974, #6035, #6046, #6050, #6061, #6073, #6076, #6086 | | [@mugni-rukita](https://github.com/mugni-rukita) | #5998 | | [@nickwizard](https://github.com/nickwizard) | #5905, #5907 | | [@ofekbetzalel](https://github.com/ofekbetzalel) | direct commit / report | | [@pizzav-xyz](https://github.com/pizzav-xyz) | #6082 | | [@powellnorma](https://github.com/powellnorma) | direct commit / report | | [@ricatix](https://github.com/ricatix) | #5993 | | [@ryanngit](https://github.com/ryanngit) | #5995 | | [@studyzy](https://github.com/studyzy) | #6043 | | [@tantai-newnol](https://github.com/tantai-newnol) | #5968 | | [@Thinkscape](https://github.com/Thinkscape) | #6088, #6090, #6091 | | [@tn5052](https://github.com/tn5052) | #5965 | | [@TuanNguyen0708](https://github.com/TuanNguyen0708) | direct commit / report | | [@vittoroliveira-dev](https://github.com/vittoroliveira-dev) | #5954 | | [@waguriagentic](https://github.com/waguriagentic) | direct commit / report | | [@whale9820](https://github.com/whale9820) | #5936 | | [@WslzGmzs](https://github.com/WslzGmzs) | direct commit / report | | [@yusufrahadika](https://github.com/yusufrahadika) | #5933 | | [@diegosouzapw](https://github.com/diegosouzapw) | maintainer | --- ## [3.8.43] — 2026-07-02 ### ✨ 新功能 - **usage(配额百分比 + 服务商 USD 成本明细):** `@@om-usage` 和 HTTP 用量端点现在以**剩余百分比**的形式报告个人 API 密钥配额(USD 金额不出现在命令输出中),服务商配额剩余量会根据配置的配额截断值进行缩放,使受保护预留部分显示为剩余 0%,配额仪表盘恢复了**服务商 USD 成本明细**功能(`/api/usage/provider-window-costs` + `ProviderUsdCostModal`,需管理认证)。同时支持**观测到的服务商配额重置**:检测到相同 `resetAt` 的重置事件(用量回落到重置基准线)时,优先采用此观测值而非陈旧的每周记录事件,用于服务商 USD 窗口和 API 密钥 USD 配额。新增 `src/lib/usage/providerWindowCosts.ts`。回归测试:`tests/unit/provider-window-costs.test.ts`、`tests/unit/internal-usage-command.test.ts`、`tests/unit/api-key-usage-limits.test.ts`、`tests/unit/lib/quota-reset-events.test.ts`。提取自 [#5863](https://github.com/diegosouzapw/OmniRoute/pull/5863),由 [@Witroch4](https://github.com/Witroch4) 贡献。 - **dashboard(反向代理后的实时 WebSocket):** 实时仪表盘 WebSocket 现在可以通过 `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL` 被反向代理或 Cloudflare Tunnel 前置(例如 `wss://ws.my-ai.com/live-ws`)。该 URL 在构建时(环境变量内联到打包产物)和**运行时**(对于预构建的 Docker/npm 镜像)均生效:`/api/v1/ws?handshake=1` 握手现在会回显一个延迟读取的 `live.publicUrl`(仅接受 `ws://`/`wss://` 值,其他值被拒绝为 `null`),`useLiveDashboard` 在连接前从该握手解析 URL,回退到之前的 `ws(s)://hostname:20129` 默认值。同时文档化了 `LIVE_WS_ALLOWED_HOSTS`,并将 `.env.example` 中的 GitLab Duo OAuth 权限范围行与实时配置保持一致(`ai_features read_user`)。回归测试:`tests/unit/live-ws-public-url.test.ts`(5 个)。([#5877](https://github.com/diegosouzapw/OmniRoute/pull/5877),由 [@ianriizky](https://github.com/ianriizky) 贡献) - **providers(CLI 配置文件自动同步):** 可选的开关,用于在服务商模型同步后自动重新生成 CLI 工具配置文件。启用后,模型目录的变更会从实时目录中(重新)写入该工具的配置文件 — Codex(`~/.codex/*.config.toml`)以及现在的 **Claude Code**(`~/.claude/profiles//settings.json`,通过提取的 `syncClaudeProfilesFromModels` + 新增的 `claudeProfileAutoSync.ts`,镜像 Codex 路径)。两者**默认关闭**,且绝不会触碰活动/默认的 CLI 配置;它们由 `OMNIROUTE_AUTO_SYNC_CODEX_PROFILES` / `OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES` 功能标志控制(DB/仪表盘覆盖 > 环境变量 > 默认 "false"),并额外受到现有 `CLI_ALLOW_CONFIG_WRITES` 写入保护的限制。CLI Code 仪表盘上新增**"CLI 配置文件自动同步"**卡片,可分别切换每个选项(从服务商仪表盘移至 [#5778](https://github.com/diegosouzapw/OmniRoute/pull/5778) — 感谢 [@rdself](https://github.com/rdself))。回归测试:`tests/unit/claude-profile-auto-sync-gate.test.ts`、`tests/unit/codex-profile-auto-sync-gate.test.ts`、`tests/unit/cli/setup-claude.test.ts`(#5737 的后续工作)。 - **cli(启动横幅):** `serve` 启动横幅现在在 ASCII 标志下方打印正在运行的 OmniRoute 版本(`v3.8.x`),使活动版本一目了然,无需单独的 `--version` 调用。回归测试:`tests/unit/cli-serve-version-banner.test.ts`。感谢 [@chirag127](https://github.com/chirag127)([#5752](https://github.com/diegosouzapw/OmniRoute/pull/5752))。 - **analytics(订阅费用):** 固定费率服务商现在在成本分析中显示 **$0**,而非虚高的按 token 估算值。订阅/编程计划服务商(所有 cookie-web 服务商 — ChatGPT Web、grok-web 等 — 以及专用的 **Minimax Coding**、**Kimi Coding**、**GLM Coding**、**Alibaba Coding Plan**、**Xiaomi MiMo** 计划)按固定费用计费而非按 token 计费,但仍然携带用于估算的按 token 定价行 — 因此分析仪表盘之前会高估其成本。新增的固定费率分类器(`src/lib/usage/flatRateProviders.ts`)通过可选的 `flatRateAsZero` 成本选项被分析界面(分析路由、用量统计、用量分析)查询,使这些服务商显示 $0,同时**预算/配额/路由的估算保持不变**。刻意不归零的:`codex`/`cx`(OmniRoute 主动跟踪 Codex token 成本 — Fast 层级乘数、GPT-5.x 定价 — 且 Codex 可能是按量计费账户)、`byteplus`(按量计费的 ModelArk)、`minimax-cn`(按量计费的中国 API)。回归测试:`tests/unit/flat-rate-cost-5552.test.ts`。([#5552](https://github.com/diegosouzapw/OmniRoute/issues/5552)) - **mcp(RTK):** 将 RTK 工具输出**学习/发现**工作流暴露为两个新的 MCP 工具,使代理无需离开协议即可扩展 RTK 过滤器目录。`omniroute_rtk_discover` 分析最近捕获的原始工具输出(`discoverRepeatedNoise` / `suggestFilter`)并返回候选噪声模式和建议的过滤器;`omniroute_rtk_learn` 列出捕获的命令样本(`listRtkCommandSamples`)并将命令解析为其 RTK 过滤器 ID(`commandToId`)。两者均为只读(作用域 `read:compression`),封装现有的 RTK 发现原语(引擎中无新逻辑),并记录到 MCP 审计跟踪。回归测试:`tests/unit/compression/rtk-mcp-tools.test.ts`(4 个)。v3.8.42 差距 — T07。 - **compression(LLM 层级):** 新增**可选、默认关闭的 LLM 层级压缩引擎**(`llm`),通过可插拔的聊天补全后端压缩非系统消息的文本。它镜像 `llmlingua` 引擎的契约,但**构造上更安全**:默认后端是**无操作透传**(在操作者同时启用它并通过 `setLlmCompressorBackend()` 连接真实后端之前,引擎不会改变有效载荷),它**不**属于默认堆叠管道的一部分,`enabled` 默认为 `false`,代码块和 `system` 消息绝不会发送给模型,每次后端错误都**安全开放**(原始片段/正文被保留,绝不丢弃)。`minTokens` 阈值跳过小型提示。真实生产后端有意作为 VPS 验证的后续工作(Hard Rule #18),与 `llmlingua` 工作后端受限的方式完全相同。新增 `open-sse/services/compression/engines/llm/index.ts`。回归测试:`tests/unit/compression/llm-compressor-engine.test.ts`(8 个)。v3.8.42 差距 — T05/C3。 - **memory(类型化衰减):** 新增**可选类型化记忆衰减**(TV6),使对话记忆存储不再累积陈旧的 `episodic` 噪声。每条注入的记忆现在跟踪 `access_count` + `last_accessed_at`(始终开启、非破坏性遥测;迁移 `111_memory_typed_decay`),以及**可选、默认关闭**的清理(`MEMORY_TYPED_DECAY_ENABLED`,默认 `false`)会删除超过**每类型 TTL** 且未被豁免的记忆。只有 `episodic` 默认衰减(30 天,可通过环境变量调整);`factual`/`procedural`/`semantic` 被豁免,任何访问次数 `>= 3` 的记忆获得访问豁免(镜像"护栏/惯例/决策永不衰减")。衰减时钟基于最后一次访问重置,因此被使用的记忆会留存。删除复用 `deleteMemory`(SQLite + sqlite-vec + Qdrant 保持同步)且安全开放;可选的定期清理是双重选择的(还需要 `MEMORY_TYPED_DECAY_SWEEP_INTERVAL>0`)。标志关闭时不会删除任何内容(Rule #20 精神)。新增 `src/lib/memory/typedDecay.ts`。回归测试:`tests/unit/memory/typed-decay.test.ts`(15 个)。v3.8.42 差距 — T10/TV6。 - **dashboard(组合):** 命名组合编辑器现在允许**拖拽排序**堆叠压缩管道,而不仅限于编辑固定位置的步骤。新增纯模型(`src/shared/components/compression/compressionPipelineModel.ts`)管理添加/移除/移动/更新,保持引擎→强度不变性和永不为空的保证,`@dnd-kit/sortable` 编辑器(`CompressionPipelineEditor.tsx`,匹配侧边栏重新排序模式)替代了 `CompressionCombosPageClient` 中的内联列表。顺序通过现有的组合端点持久化。回归测试:`tests/unit/compression-pipeline-model.test.ts`(11 个)+ `tests/unit/ui/compression-pipeline-editor.test.tsx`(4 个)。专用的 `tests/e2e/compression-studio.spec.ts`(Tela A 渲染 + 选项卡切换)填补了组合实时规范未覆盖的压缩工作室端到端测试空白。v3.8.42 差距 — T06 + T03。 - **compression(管道):** 为堆叠压缩管道新增**可选、默认关闭的每引擎断路器**(T02)。当引擎在**跨请求**中反复抛出异常时,其断路器打开,堆叠循环跳过该引擎(保持正文原样传递给该步骤 — 安全开放),冷却后探测一次(惰性半开);成功则关闭,探测失败则重新打开。这与服务商断路器不同(`src/shared/utils/circuitBreaker.ts`,服务商级别 + DB 持久化)— 新增的 `pipelineEngineBreaker.ts` 是引擎级别的、进程本地的,在热路径上零 DB/IO 开销。它与现有的每请求 TV1 保释机制(跳过单个请求内的步骤)配合使用;断路器增加了跨请求的记忆。**默认关闭**(`COMPRESSION_PIPELINE_BREAKER_ENABLED=false`)→ 与断路器前的管道字节级相同(除非 TV1 单独启用,否则抛出异常的引擎仍会传播)。可通过每次调用、每个 `CompressionConfig` 或环境变量(`_THRESHOLD`/`_COOLDOWN_MS`)配置。回归测试:`tests/unit/compression/pipeline-circuit-breaker.test.ts`(9 个,包含抛出异常引擎的集成测试);现有的 strategySelector/保释测试套件保持绿色。v3.8.42 差距 — T02(2.2)。 - **compression(CCR):** CCR 检索反馈(H8)现在是**渐进式**的,而非二值悬崖。之前检索 `>= 3` 次的块被标记为不压缩,以下所有内容保持完全可压缩。现在每次先前的检索会**线性提高**块的**有效 `minChars`**(`effectiveMinChars`),因此频繁检索的内容压缩程度逐渐降低;`>= 3` 次排除被保留(作为 `Infinity`)。斜率由 `retrievalRampFactor` 控制(默认 `2`,每个组合配置或 `COMPRESSION_CCR_RETRIEVAL_RAMP_FACTOR`);`1` 可重现确切的旧版二值行为。每 `(principal, hash)` 的隔离不变。回归测试:`tests/unit/compression/ccr-retrieval-ramp.test.ts`(12 个);现有 CCR 套件(51 个)保持绿色。v3.8.42 差距 — T08/H8。 - **compression(缓存感知):** 新增**可选、默认关闭的基于使用量的前缀冻结**(H5)。缓存感知保护之前仅对静态启发式识别为缓存的提供者保留系统提示。现在它还会**学习**哪些系统提示实际上会重复出现:一旦系统提示在跨请求中被观察到 `>=` 某个阈值次数,它就被视为稳定的可缓存前缀,并受到压缩保护 — **即使对于静态检查遗漏的提供者也是如此** — 恢复前缀压缩模式会破坏的提示缓存命中。通过系统提示的哈希进行内容寻址(OpenAI / Claude / Gemini 格式),内存中 + 有界,零 DB/IO;"冻结"仅保留前缀,因此永远不会改变有效载荷。默认关闭(`COMPRESSION_PREFIX_FREEZE_ENABLED`,阈值 `_THRESHOLD`);遵守 `never` 保留模式(永不解冻)。新增 `open-sse/services/compression/prefixFreeze.ts`,接入 `resolveCacheAwareConfig`。回归测试:`tests/unit/compression/prefix-freeze.test.ts`(10 个);44 个现有缓存感知/保留模式测试保持绿色。v3.8.42 差距 — T08/H5。 - **compression(读取生命周期):** 新增**可选、默认关闭的 `read-lifecycle` 引擎**(H7),用于折叠**过时/被取代的文件读取工具结果**。在代理对话中,同一个文件被反复读取;一旦同一路径被**重新读取**(被更新的视图取代)或被后续的写入/编辑**修改**,先前的读取就变为过时。该引擎用简短占位符替换那些先前的读取结果 — 仅保留当前(最后、未被取代的)读取完整 — 回收模型不再需要的 token。与 `session-dedup`(相同内容)或 `ccr`(可逆标记)不同,这是语义性的 + **有损的**,因此是可选功能(`enabled` 默认 `false`)。构造上保守:仅匹配已知的读取/写入工具名称,比较精确路径,仅在严格较晚的调用触及相同路径时折叠读取,遇到任何意外形状时**安全开放**。支持 **Anthropic**(`tool_use`/`tool_result`)和 **OpenAI**(`tool_calls` + `role:"tool"`)两种格式。新增 `open-sse/services/compression/engines/readLifecycle/index.ts`。回归测试:`tests/unit/compression/read-lifecycle.test.ts`(10 个)。v3.8.42 差距 — T08/H7。 - **observability(关联 ID):** 请求现在携带关联 ID,贯穿日志,使单个请求可以在管道中端到端追踪。([#5834](https://github.com/diegosouzapw/OmniRoute/pull/5834) — 感谢 @hartmark) - **cli(启动横幅 — 启动耗时):** `serve` 就绪横幅现在显示启动耗时,使慢启动情况一目了然。([#5799](https://github.com/diegosouzapw/OmniRoute/pull/5799) — 感谢 @ishatiwari21) - **api(配额策略绕过作用域):** 新增**可选**的 API 密钥服务商配额策略绕过作用域,使指定密钥可在不禁用全局配额的情况下免于服务商配额执行。([#5731](https://github.com/diegosouzapw/OmniRoute/pull/5731) — 感谢 @Witroch4) - **providers(Ollama 本地):** 在服务商仪表盘上新增一流的 **Ollama** 本地服务商卡片,使本地 LLM 运行时可以像其他服务商一样配置。([#5712](https://github.com/diegosouzapw/OmniRoute/pull/5712) — 感谢 @diegosouzapw) - **codex(回退配置文件):** 为 Codex 兼容模型生成回退 CLI 配置文件,使兼容模型自动获得可用的配置文件。([#5701](https://github.com/diegosouzapw/OmniRoute/pull/5701) — 感谢 @skyzea1) - **api(响应体验证 + 故障转移):** 新增**可配置的响应体验证**步骤,当上游返回结构无效的响应体时,可将该目标故障转移到下一个候选项(routing/#4985)。([#5684](https://github.com/diegosouzapw/OmniRoute/pull/5684) — 感谢 @diegosouzapw) - **providers(SenseNova):** 完善 SenseNova 免费**Token 计划** — 聊天补全加上**文本到图像**(从 9router#2233 移植)。([#5679](https://github.com/diegosouzapw/OmniRoute/pull/5679) — 感谢 @diegosouzapw) - **db(自校正上下文窗口):** 新增**自校正模型上下文窗口覆盖**,使广告的上下文长度有误的模型能自动纠正(models/#5004)。([#5667](https://github.com/diegosouzapw/OmniRoute/pull/5667) — 感谢 @diegosouzapw) - **routing(延迟策略):** 使用观测到的每目标性能指标优化延迟路由策略,以获得更好的候选选择。([#5629](https://github.com/diegosouzapw/OmniRoute/pull/5629) — 感谢 @KooshaPari) - **compression(preserveSystemPrompt 模式):** 新增 `preserveSystemPrompt` 模式枚举(`always` | `whenNoCache` | `never`),保留旧版兼容性,使操作者可以明确控制系统提示何时受到压缩保护(T05/C5)。([#5653](https://github.com/diegosouzapw/OmniRoute/pull/5653) — 感谢 @diegosouzapw) - **commandCode(视觉):** 为 Command Code 视觉模型新增多模态**图像**支持。([#5557](https://github.com/diegosouzapw/OmniRoute/pull/5557) — 感谢 @Stazyu) - **compression(读取生命周期引擎):** T08/H7(2.5)— 可选读取生命周期引擎,折叠被取代的文件读取,使同一文件的过时早期读取从上下文中被裁剪。([#5754](https://github.com/diegosouzapw/OmniRoute/pull/5754) — 感谢 @diegosouzapw) - **compression(基于使用量的前缀冻结):** T08/H5(2.4)— 由观测到的使用量驱动的可选前缀冻结,保持稳定的缓存前缀不被下游引擎重写。([#5744](https://github.com/diegosouzapw/OmniRoute/pull/5744) — 感谢 @diegosouzapw) - **compression(CCR 检索反馈斜率):** T08/H8(2.3)— 渐进式上下文压缩比检索反馈斜率,根据检索信号调整压缩激进程度。([#5739](https://github.com/diegosouzapw/OmniRoute/pull/5739) — 感谢 @diegosouzapw) - **compression(每引擎断路器):** T02 — 可选每引擎管道断路器,禁用行为异常的压缩引擎而不使整个管道失败。([#5735](https://github.com/diegosouzapw/OmniRoute/pull/5735) — 感谢 @diegosouzapw) - **compression(LLM 层级引擎):** T05/C3 — 可选 LLM 层级压缩引擎,使用模型通道进行更高比率的语义压缩。([#5702](https://github.com/diegosouzapw/OmniRoute/pull/5702) — 感谢 @diegosouzapw) - **dashboard(压缩管道编辑器):** T06/T03 — 拖拽排序的压缩管道编辑器加上压缩工作室端到端流程。([#5727](https://github.com/diegosouzapw/OmniRoute/pull/5727) — 感谢 @diegosouzapw) - **memory(类型化衰减):** T10/TV6 — 可选类型化记忆衰减,使陈旧、低价值的记忆按类型时间表逐渐消失。([#5723](https://github.com/diegosouzapw/OmniRoute/pull/5723) — 感谢 @diegosouzapw) - **mcp(RTK 工具):** T07 — 将 RTK 学习/发现能力作为一流的 MCP 工具暴露。([#5691](https://github.com/diegosouzapw/OmniRoute/pull/5691) — 感谢 @diegosouzapw) - **providers(CLI 配置文件自动同步):** 可选 CLI 配置文件自动同步开关,包括 Claude Code 自动同步,使生成的 CLI 配置文件可以自动跟踪服务商变更。([#5755](https://github.com/diegosouzapw/OmniRoute/pull/5755) — 感谢 @diegosouzapw) ### 🔧 问题修复 - **fix(opencode):** 停止在客户端未发送时伪造 `User-Agent: opencode/local` 和 `x-opencode-client: cli` 请求头 — executor 去重重构([#5720](https://github.com/diegosouzapw/OmniRoute/pull/5720))意外地重新引入了请求头伪造,违反了仅转发契约(发明 opencode 内部值有被上游拒绝的风险)。恢复为仅转发:仅当存在真实客户端来源时才发送这些请求头。回归测试:`tests/unit/opencode-executor.test.ts`。(感谢 @diegosouzapw) - **fix(executors):** `resolveEffectiveKey` 在没有 API 密钥时返回 `undefined`(而非 `""`)— 类型强制清理([#5798](https://github.com/diegosouzapw/OmniRoute/pull/5798))将 `apiKey ?? ""` 改为满足类型检查器,静默地改变了认证密钥解析语义。将返回类型加宽为 `string | undefined` 并撤销了强制转换,使仅 OAuth 凭据能正确解析。回归测试:`tests/unit/refactor-buildHeaders-preamble.test.ts`。(感谢 @diegosouzapw) - **fix(translator):** 恢复 Responses→Claude 流上的终端 `message_delta` + `message_stop` — 工具参数重复去重([#5828](https://github.com/diegosouzapw/OmniRoute/pull/5828))在共享的 `state.finishReason` 上守卫了完成处理程序,而 openai-responses→openai 通道在枢纽路径中首先设置该值,因此 openai→claude 通道丢弃了其终端事件,流在 `content_block_delta` 后结束。去重现在使用专用的 `state.claudeFinishEmitted` 标志。回归测试:`tests/unit/claude-code-rendering-fixes.test.ts`。(感谢 @diegosouzapw) - **fix(pricing):** 新增 Kiro `claude-sonnet-5` 定价行,使新编目的模型([#5796](https://github.com/diegosouzapw/OmniRoute/pull/5796))不再报告 `$0.00` 用量。回归测试:`tests/unit/catalog-updates-v3x.test.ts`。(感谢 @diegosouzapw) - **fix(github):** 保持 Copilot 访问令牌会话活跃。GitHub Copilot 设备流账户可能持有 GitHub 访问令牌加上短期 Copilot 令牌,而**没有**刷新令牌;主动健康检查将其视为终端 `no_refresh_token`,并在登录后几分钟标记连接过期。健康检查现在保持这些会话活跃,清除过时的 `no_refresh_token` 状态,并在需要时刷新 Copilot 子令牌。回归测试:`tests/unit/token-health-no-refresh-token-expired-5326.test.ts`。提取自 [#5863](https://github.com/diegosouzapw/OmniRoute/pull/5863),由 [@Witroch4](https://github.com/Witroch4) 贡献。 - **fix(kiro):** 将 Claude 模型 ID 破折号→点号规范化限制为 1–2 位副版本号,使日期后缀 ID(例如 claude-opus-4-20250514)不再被损坏。(感谢 @voravitl) - **fix(usage):** 即使在请求体被截断时,也在请求日志中保留(有界的)工具定义,使请求详情视图仍能显示可用工具。(感谢 @noir017) - **fix(providers):** 将 OpenAI 仅响应模型路由到 `/v1/responses` 而非在 `/v1/chat/completions` 上 404。策划的 `gpt-5.5-pro` / `gpt-5.4-pro` 条目从未工作过(OpenAI 仅通过 Responses API 提供 `*-pro` 推理模型),"测试所有模型"也显示相同的 404。注册表条目现在携带 `targetFormat: "openai-responses"`(复用与 `gh`/`codex` 共享的现有每模型翻译管道),`DefaultExecutor.buildUrl` 将 `openai` 端点同步替换为 `/responses`(遵循自定义基础 URL),`-pro` 后缀启发式覆盖动态同步的 ID,如 `o1-pro` / `gpt-5.2-pro`(与 gh executor 的 `/codex/i` 路由相同精神,9router#102)。旧版仅补全 ID(例如 `gpt-3.5-turbo-instruct`)不在范围内 — 它们不在目录中,OmniRoute 也没有旧版 `/v1/completions` 上游。回归测试:`tests/unit/openai-responses-only-models-5842.test.ts`(8 个)。感谢 [@maikokan](https://github.com/maikokan)。([#5842](https://github.com/diegosouzapw/OmniRoute/issues/5842)) - **fix(image):** 保持裸 codex 图像别名(例如 `gpt-5.5`)解析到 codex 图像管道,即使组合共享相同名称。名为 `gpt-5.5` 的聊天组合曾经在 `resolveImageRouteModel` 中遮蔽裸图像别名,劫持 `/v1/images/*` 请求到聊天目标(与 [#5887](https://github.com/diegosouzapw/OmniRoute/issues/5887) 相邻的回归路径);codex 裸模型现在在裸组合解析之前被保留,而非 codex 别名(例如 `gpt-image-2`)仍可被用户遮蔽(#3214/#3215 行为保留)。回归测试:`tests/unit/image-routes-combo-edits-3214-3215.test.ts`(9 个)。([#5902](https://github.com/diegosouzapw/OmniRoute/pull/5902),由 [@KooshaPari](https://github.com/KooshaPari) 贡献) - **fix(ci):** 重新打通 `release/v3.8.43` 快速门禁队列 — 每个 PR→发布都在继承基础红色([#5798](https://github.com/diegosouzapw/OmniRoute/issues/5798))。清除了五个不同的阻塞项:(1) `check:db-rules` 有意内部化白名单中过时的 `modelContextOverrides` 条目([#5827](https://github.com/diegosouzapw/OmniRoute/pull/5827) 将其加入白名单,而 [#5609](https://github.com/diegosouzapw/OmniRoute/issues/5609) 修复将其从 `localDb.ts` 重新导出;重新导出保留,过时条目移除,分类守卫重新固定为 33);(2) `LIVE_WS_ALLOWED_HOSTS` / `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL` 在 `docs/reference/ENVIRONMENT.md` 中文档化(环境/文档契约,来自 [#5877](https://github.com/diegosouzapw/OmniRoute/pull/5877));(3) 路由器后端 ADR 对尚未合并的注册表([#5868](https://github.com/diegosouzapw/OmniRoute/pull/5868))的引用标记为随 PR 落地,使 `check:fabricated-docs --strict` 通过;(4) `antigravity-429-quota-tdd` + `middleware-header-strip-5849` 添加到 stryker `tap.testFiles`(`check:mutation-test-coverage`);(5) 文件大小/复杂度/认知复杂度基准重新设定,附有理由说明 — 所有漂移在原始分支尖和此 PR 上测量完全一致(净零)。回归测试:`tests/unit/check-db-rules-classification.test.ts`。([#5798](https://github.com/diegosouzapw/OmniRoute/issues/5798)) - **providers(codex 图像自动路由回归):** 来自仅 codex 设置(无 OpenAI 连接)的无前缀 `gpt-5.5` 请求现在再次正确推断 `codex` 提供者 — OpenAI 静态目录在 `resolveModelByProviderInference` 中的短路正在抢占 codex 偏好块,因此 `gpt-5.5`(已添加到 OpenAI 目录)停止自动路由到 Codex 图像生成。具有活跃 OpenAI 连接的用户不受影响(OpenAI 保持默认)。回归测试:`tests/unit/codex-gpt55-routing-5887.test.ts`。([#5887](https://github.com/diegosouzapw/OmniRoute/issues/5887)) - **api(代理请求头清理):** 上游 `x-middleware-*` 控制请求头(由托管在 Next.js 后面的提供者发出,例如 synthetic.new)现在从代理响应中被剥离,而非逐字转发 — 转发 `x-middleware-rewrite` 使 Next 16 抛出 `NextResponse.rewrite() was used in a app route handler` 并返回 500,尽管上游调用成功。适用于流式和 JSON 路径。回归测试:`tests/unit/middleware-header-strip-5849.test.ts`。([#5849](https://github.com/diegosouzapw/OmniRoute/issues/5849)) - **docs(pnpm 全局安装):** 将不受支持的 `pnpm approve-builds -g` 步骤替换为安装时的 `pnpm add -g omniroute@latest --allow-build=better-sqlite3` 标志,覆盖 README + 设置指南(及 i18n 镜像),修复 pnpm v11 全局安装的原生构建批准问题。([#5554](https://github.com/diegosouzapw/OmniRoute/issues/5554)) - **dashboard(令牌徽章):** 红色"令牌已过期"连接徽章不再对支持 OAuth 刷新的提供者(Antigravity/Gemini)闪烁,这些提供者的访问令牌仅过期但会自动刷新 — 现在仅在连接终端过期时显示(`testStatus === "expired"`)。#5326 的延续。回归测试:`tests/unit/ui/connection-row-token-badge-5836.test.tsx`。([#5836](https://github.com/diegosouzapw/OmniRoute/issues/5836)) - **db(自动备份开关):** 完整的写入前 SQLite 备份现在遵循持久化的 `backup.autoBackupEnabled` 仪表盘设置 — 之前只检查 `DISABLE_SQLITE_AUTO_BACKUP` 环境变量,因此在 UI 中禁用自动备份无效,约 70MB 的写入前快照仍在不断触发。手动和恢复前备份仍然始终运行。回归测试:`tests/unit/db-backup-autobackup-setting-5871.test.ts`。([#5871](https://github.com/diegosouzapw/OmniRoute/issues/5871)) - **providers(自定义提供者的 auto/ 路由):** 自定义 OpenAI-/Anthropic-兼容提供者(动态 `*-compatible-*` 连接 ID)不再被排除在 `auto/` 路由之外 — Auto-Combo 虚拟工厂之前跳过任何提供者不在静态注册表中的连接。现在回退到连接的 `defaultModel`。回归测试:`tests/unit/auto-custom-provider-5873.test.ts`。([#5873](https://github.com/diegosouzapw/OmniRoute/issues/5873)) - **middleware(Hook 沙箱):** 操作者编写的预请求 Hook 代码现在在强化的 Node `vm` 沙箱中运行(最小上下文、无环境全局变量/`process.env`、执行超时、无 `require`),而非在主进程中通过 `new Function()` — 关闭了 Hard Rule #3 / SonarCloud S1523 暴露面。回归测试:`tests/unit/middleware-hook-sandbox-5872.test.ts`。([#5872](https://github.com/diegosouzapw/OmniRoute/issues/5872)) - **mcp-server(认证转发):** 通过 `withMcpHttpAuthContext` 转发的每调用者 MCP 身份现在优先于内部 fetch 辅助函数(`apiFetch`、`omniRouteFetch`)中的静态 `OMNIROUTE_API_KEY` 环境变量回退 — 之前环境变量密钥在转发请求头之后被展开,覆盖了调用者的 `Authorization`。回归测试:`open-sse/mcp-server/__tests__/httpAuthContext.test.ts`。([#5819](https://github.com/diegosouzapw/OmniRoute/issues/5819)) - **dashboard(Modal 提供者 — 双字段认证):** Modal 提供者连接表单现在暴露**两个字段 — Token ID + Token Secret —** 而非单一的 API 密钥输入,因为 Modal 使用 `Authorization: Bearer :` 进行认证。仪表盘将两个字段合并为 `id:secret` 凭据后保存(`combineModalCredential`,修剪两部分),而在旧版单字段格式中粘贴的值保持逐字工作(空 secret → 透传),因此现有已保存的连接无需迁移;密钥帮助链接指向 Modal 的令牌设置。回归测试:`tests/unit/modal-credential-combine.test.ts`(5 个)。([#5881](https://github.com/diegosouzapw/OmniRoute/pull/5881),关闭 [#5446](https://github.com/diegosouzapw/OmniRoute/issues/5446))后续:**验证模型 ID** 字段现在为 Modal 预填了服务端验证器探测的相同模型(`Qwen/Qwen3-4B-Thinking-2507-FP8`,通过 `src/shared/constants/modal.ts` 中的 `MODAL_DEFAULT_VALIDATION_MODEL_ID` 共享),关闭 #5446 的最后一项清单条目。回归测试:`tests/unit/modal-validation-model-prefill.test.ts`。 - **api(聊天补全 — 早期 SSE 保活门控):** `/v1/chat/completions` 路由在 `stream` 未显式为 `false` 时将响应包装在早期流保活中,因此省略 `stream` 并请求 JSON 的客户端(`Accept: application/json`)可能收到过早的 SSE 帧。保活包装器现在受显式 `stream: true` 或强制 SSE 的 Accept 请求头(`acceptHeaderForcesStream`)的门控;解析后的请求体原样传递给聊天处理程序,因此实际的流/JSON 帧仍由 `chatCore`/`resolveStreamFlag` 决定 — 保留 OmniRoute 在省略 `stream` 时的旧版流默认行为和每密钥 `streamDefaultMode: "json"` 选择。回归测试:`tests/unit/chat-combo-live-test.test.ts`("省略 stream 且 Accept 为 application/json 时返回 JSON 而无早期 SSE 帧")。([#5866](https://github.com/diegosouzapw/OmniRoute/pull/5866),由 [@rdself](https://github.com/rdself) 贡献) - **fix(github):** 在派发到 GitHub Copilot 聊天前丢弃尾随的 assistant 预填充,以避免 400 错误。(感谢 @baslr) - **fix(oauth):** 通过在存在 `username` 时以此字段而非仅用 email 来消除 OAuth 连接歧义,防止跨 IdP 账户覆盖。(感谢 @KunN-21) - **fix(mitm):** 在 sudo 密码缓存时尽力在退出时还原特权 /etc/hosts 条目,而非始终留下孤立状态。(感谢 @manhdzzz) - **providers(Kiro — Claude Sonnet 5):** Kiro 提供者的模型目录缺少 `claude-sonnet-5`,因此即使账户已有访问权限,该模型也无法被选择或路由("claude-sonnet-5 is not supported")。将模型添加到 Kiro 注册表(`open-sse/config/providers/registry/kiro/index.ts`),作为 1M 上下文 / 128K 输出的 Claude 模型,镜像现有 Claude 条目;注册表 `models[]` 同时供给模型选择器和实时 CodeWhisperer `ListAvailableModels` 回退,因此该模型现在可选择和可路由。回归测试:`tests/unit/kiro-claude-sonnet-5-2267.test.ts`。(感谢 [@openbioinfo](https://github.com/openbioinfo)) - **settings(模型别名 — 重启后自愈):** 设置 → 路由页面在服务器重启后显示"未配置精确匹配别名",即使别名已持久化在数据库中。别名保存在 `modelDeprecation.ts` 的模块本地 `_customAliases` 映射中,启动路径会对其进行水合,但 Next.js 将应用路由模块图与启动图分开编译(与 #5312 相同的 webpack 块分割类),因此 `GET /api/settings/model-aliases` 处理程序读取了不同的、未水合的副本。处理程序现在可以自愈:当其内存中别名映射为空时,它从 DB 读取 `settings.modelAliases`(通过现有的 `getSettings()` db 模块 — 路由中无原始 SQL)并重新填充映射,因此 UI 在重启后的第一次 GET 上就能反映持久化的别名。后续:根本原因现也已修复 — `modelDeprecation.ts` 中的 `_customAliases` 存储由 `globalThis` 支持(键 `__omniroute_customAliases__`),因此启动和应用路由模块图共享**一个**存储,路由直接读取启动水合的别名(DB 自愈保留为无害的回退),镜像已应用于 `thinkingBudget.ts`/`backgroundTaskDetector.ts`(#5312)的相同 `globalThis` 单例模式。回归测试:`tests/unit/model-aliases-settings-route-selfheal.test.ts` + `tests/unit/model-aliases-globalthis-5777.test.ts`。([#5777](https://github.com/diegosouzapw/OmniRoute/pull/5777) — 感谢 [@jleonar2](https://github.com/jleonar2)) - **providers(grok-cli 令牌自动刷新):** grok-cli OAuth 令牌在其实际过期之前从未被主动刷新。`mapTokens` 硬编码 `expiresIn: 21600`(6 小时),无论令牌的实际生命周期如何,因此持久化的 `expiresAt` 始终是"现在 + 6 小时",主动 `tokenHealthCheck` 清理(在 `expiresAt - now < 5 min` 时刷新)在导入后 6 小时触发,而非在令牌真正过期前不久。`mapTokens` 现在从 `~/.grok/auth.json` 中的权威 `expires_at` 字段(ISO → epoch 秒)计算 `expiresIn`,回退到 JWT `exp` 声明(仅有效载荷解码,无签名信任);硬编码的 `21600` 仅在两者都不存在时保留。已过期的令牌(实际 `expires_at`/`exp` 在过去)现在通过 `Math.max(1, …)` 被钳位为正的 `expiresIn`,因此导入路由存储一个近未来的 `expiresAt`,AutoCombo 刷新连接而非读取过去的日期并完全排除它。回归测试:`tests/unit/grok-cli-oauth.test.ts` 中 5 个用例(JWT `exp`、JSON `expires_at`、`21600` 回退,以及两个过期令牌钳位)。([#5775](https://github.com/diegosouzapw/OmniRoute/pull/5775) — 感谢 [@Chewji9875](https://github.com/Chewji9875)) - **compression(通过 MCP HTTP 的 CCR 检索):** `omniroute_ccr_retrieve` MCP 工具在通过 MCP HTTP 传输(SSE / Streamable HTTP)调用时,对在**同一**会话中存储的块返回 `"CCR block not found"`,例如从 Docker 部署中的 OpenCode 调用。压缩存储按 API 密钥主体键控每个块(`String(apiKeyInfo.id)`),但工具通过 `extra.authInfo.clientId` 解析调用者 — MCP SDK 在 API 密钥认证时从不填充此字段 — 因此它回退到 `"anonymous"`,复合存储键从不匹配。检索工具现在从 MCP HTTP 认证上下文(`httpAuthContext`)使用存储时所用的**相同** `getApiKeyMetadata` 查找来解析调用者的 API 密钥 ID,因此检索与存储匹配。跨租户 IDOR 隔离被保留:不同密钥解析为不同 ID → 未命中;无密钥 → 仅匿名桶。回归测试:`tests/unit/compression/ccr-mcp-principal-5649.test.ts`(提取、不同主体隔离、安全关闭、端到端存储→检索)。([#5649](https://github.com/diegosouzapw/OmniRoute/issues/5649)) - **compression(上下文编辑遥测):** 流式响应现在记录上下文编辑节省量。Anthropic 在 SSE 流的最终 `message_delta` 快照上暴露 `context_management.applied_edits[]`,但流式重建(`buildStreamSummaryFromEvents` → Claude 分支)完全丢弃了 `context_management`,**且**流式终结器中未接入任何遥测 Hook — 因此委托的服务器端上下文清除节省量(`cleared_input_tokens` / `cleared_tool_uses`)仅在**非流式响应**中出现在压缩分析的引擎 `context-editing` 下。收集器现在从最终快照中保留 `context_management`(最后写入者获胜),`onStreamComplete` 镜像了非流式 `recordContextEditingTelemetryHook`(尽力而为,仅 Claude,仅 HTTP 200)。纯附加遥测 — 无有效载荷变更,无新环境变量标志,流不携带 `context_management` 时无行为变更。回归测试:`tests/unit/context-editing-streaming-telemetry.test.ts`(3 个)。v3.8.42 差距 — T01(5.1)。 - **proxy(中继测试诊断):** 代理池"测试"按钮在**中继**(Vercel / Deno / Cloudflare)响应非 200 时显示简单的"失败",且**服务器日志中没有任何内容** — 例如 `STORAGE_ENCRYPTION_KEY` 轮换后的认证令牌不匹配导致 `401`。中继成功路径响应设置了 `success: false` 但不携带 `error` 字段,因此仪表盘没有理由显示,服务器也未记录任何内容。测试现在返回可操作的 `error`(HTTP 状态,加上对 `401`/`403` 的认证/加密密钥提示)并在服务器端记录失败;SOCKS5/HTTP 代理路径现在也记录其失败。格式提取到 `buildRelayTestResult`,带有回归测试(`tests/unit/proxy-relay-test-error-5716.test.ts`)。注意:这只是暴露中继**为什么**失败 — 不修复真正损坏/配置错误的中继。([#5716](https://github.com/diegosouzapw/OmniRoute/issues/5716)) - **fix(dashboard):** 为组合和 MITM 代理页面添加错误边界,使渲染错误显示可恢复的回退而非空白页面。(感谢 @wahyuzero) - **providers(入门向导 — 不支持的验证):** 添加凭据没有**实时验证器**的提供者(LMArena、PiAPI 等)在添加提供者向导中静默失败。`/api/providers/validate` 端点对这些返回 `HTTP 400 + { unsupported: true }`(#5565/#5567),但向导的 `validateOnboardingApiKey` 通过 `expectOk` 运行它,后者在非 200 时抛出异常 — 因此流程跳转到错误步骤,连接**从未被创建**。向导现在将 `unsupported: true` 视为非阻塞的"无法验证",并继续保存,镜像 `AddApiKeyModal`。回归测试添加到 `tests/unit/provider-onboarding-wizard.test.ts`。(相关于 [#5692](https://github.com/diegosouzapw/OmniRoute/issues/5692)) - **dashboard(快速入门步骤 1):** 快速入门"创建 API 密钥"步骤告诉用户"前往**端点** → 已注册密钥"并链接到 `/dashboard/endpoint`,但 API 密钥是在 **API 管理**页面(`/dashboard/api-manager`,侧边栏"API 密钥")创建的 — 端点页面没有"已注册密钥"部分,因此用户跟随链接后找不到在哪里创建密钥。步骤 1 现在显示"前往 **API 密钥**"并链接到 `/dashboard/api-manager`。回归测试:`tests/unit/ui/quick-start-api-keys-link-5695.test.ts`。([#5695](https://github.com/diegosouzapw/OmniRoute/issues/5695)) - **providers(DashScope/Alibaba 设置链接):** **Alibaba** 和 **Alibaba (China)** 提供者的"获取 API 密钥"链接指向裸 API 主机(`dashscope-intl.aliyuncs.com` / `dashscope.aliyuncs.com`),在浏览器中返回 **404** — API 主机名没有主页。重新指向实际发放密钥的控制台:`bailian.console.alibabacloud.com`(国际)和 `dashscope.console.aliyun.com`(中国)。与 #5572/#5574/#5576 相同类别;回归测试添加到 `tests/unit/provider-setup-links-5572.test.ts`。([#5665](https://github.com/diegosouzapw/OmniRoute/issues/5665)) - **thinking / runtime-config(模块图修复):** 在启动时水合但**每请求**读取的操作者配置代理设置在生产中静默地被忽略。Next.js 将 `instrumentation.ts`(通过 `applyRuntimeSettings` / 恢复 Hook 进行启动水合)编译为与 app-route / open-sse executor **分离的 webpack 模块图**,因此模块本地的 `let _config` 单例被**复制** — 启动副本已水合,但请求路径读取了不同的、未水合的副本。实时 VPS 验证证明 Thinking-Budget 水合在启动时完成,但 `base.ts` 仍然看到 `passthrough` 默认值(这就是 #5312 修复 A 在启动连线修复后仍然失效的原因)。通过用 `globalThis` 支持单例来修复(`systemPrompt.ts` 已用于全局系统提示的模式,#2470),因此所有模块图副本共享一个实例:**`thinkingBudget.ts`**(仪表盘 Thinking-Budget 模式现在到达 executor)、**`backgroundTaskDetector.ts`**(可选后台模型降级现在实际在请求时触发)、**`systemTransforms.ts`**(操作者管道覆盖现在到达请求路径)。`payloadRules.ts` 已安全(它每请求从 DB 惰性加载自身,#2986)。回归测试:`tests/unit/thinking-budget-globalthis-5312.test.ts` + `tests/unit/runtime-config-globalthis-5312.test.ts`(断言 globalThis 支持的共享;模块本地的 `let` 会失败)。([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312)) - **thinking(Claude OAuth):** 在启动时恢复代理级 **Thinking-Budget** 配置。仪表盘模式(`auto`/`custom`/`adaptive`)持久化在 `settings.thinkingBudget` 下,但启动时水合(`hydrateThinkingBudgetConfig`)仅接入 `src/server-init.ts` — 一个**生产中从未运行的未使用模块** — 因此操作者的选择在每次重启时静默回退到 `passthrough` 默认值(#5312 修复 A 无效,尽管其直接单元测试通过)。水合现在在实际启动路径(`src/instrumentation-node.ts`)中运行,与全局系统提示恢复并列。由 VPS 上的实时 Anthropic-OAuth 验证发现。回归测试:`tests/unit/thinking-budget-boot-wiring-5312.test.ts`(断言生产启动模块调用水合,而非仅在隔离中测试函数)。([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312)) - **translator/chatcore(加固):** 重新应用在 #5661 / #5662 之前分支重建中被丢弃的两个防御性审查修复。(1) `mergeConsecutiveSameRoleContents`(OpenAI→Gemini)现在浅拷贝每个条目及其 `parts` 数组,而非推送输入引用,因此连续相同角色合并永远不会改变调用者的对象。(2) `defaultClaudeToolType`(Claude 工具默认值)现在将任何非对象数组条目(`null` / 原语)原样传递,而非将其展开为虚构的 `{ type: "custom", … }` 工具。在真实负载上无行为变更(Gemini 内容是新构建的;Claude 工具始终是对象);两个属性现在由 `tests/unit/translator-gemini-consecutive-role-2191.test.ts` 和 `tests/unit/claude-tool-type-default-2195.test.ts` 中的回归测试锁定。 - **providers(grok-cli):** 当工具列表超过提供者的硬限制时截断工具列表,使 grok-cli(`cli-chat-proxy.grok.com`,最大 200 个工具)不再以 `Maximum tools limit reached` 拒绝请求。新增主动 `PROVIDER_TOOL_LIMITS` 映射(`grok-cli: 200`,在反应式缓存之前查询),修正的限制解析正则表达式捕获声明的最大值(`200`)而非提供的数量(`427`),并移除损坏的 `< MAX_TOOLS_LIMIT` 截断门,因此截断现在在 `tools.length` 超过有效限制时触发。回归测试:`tests/unit/tool-limit-detector.test.ts`。([#5563](https://github.com/diegosouzapw/OmniRoute/pull/5563) — 感谢 @Chewji9875) - **resilience(antigravity):** 为 Antigravity `429 rate_limit_exceeded` 错误记录模型锁定。Antigravity 的 `"Resource has been exhausted (e.g. check quota)."` 文本被过于宽泛的 `QUOTA_PATTERNS` 匹配,错误分类为 `QUOTA_EXHAUSTED`,因此组合重试路径被跳过(`providerExhausted`),模型从未被冷却。分类现在优先使用结构化错误代码 — `classifyErrorText(structuredError?.code || errorText)` — 因此 `rate_limit_exceeded` 代码被视为瞬态速率限制(非配额),两个宽泛模式(`/resource.*exhaust/i`、`/check.*quota/i`)替换为 Antigravity 特定的模式(`individual quota reached`、`enable overages`)。([#5579](https://github.com/diegosouzapw/OmniRoute/pull/5579) — 感谢 @Chewji9875) - **providers(OpenAI-compatible):** Codex MCP / `tool_search` 延迟发现(及 `apply_patch`)现在通过**自定义 OpenAI-compatible 提供者**工作。当此类提供者收到携带 MCP / `tool_search` 工具的 Responses-API 格式请求时,OmniRoute 将其降级为 `/chat/completions`,这会丢弃延迟工具发现机制 — 因此 MCP 命名空间从未暴露给模型,`apply_patch` 被错误地处理为 JSON 工具。executor 现在检测携带 `namespace` / `tool_search*` 工具的 Responses 格式请求(`input` / `previous_response_id` / `max_output_tokens` / `reasoning`),并将其原生路由到上游 `/responses` 端点而非降级(也可通过 `providerSpecificData._omnirouteForceResponsesUpstream` 强制)。这是与官方 Codex OAuth 后端(#3033 / #4539,早期修复从未触及)不同的代码路径。回归测试:`tests/unit/executor-default-base.test.ts`。感谢 @KooshaPari 的修复。([#5483](https://github.com/diegosouzapw/OmniRoute/issues/5483)) - **dashboard(路由):** 在全局路由默认选项卡上选择 **fusion** 策略现在显示 fusion 特定配置,而非仅显示通用弹性字段。Fusion 的引擎旋钮 — `judgeModel`(综合面板答案的模型)和 `fusionTuning`(`minPanel` / `stragglerGraceMs` / `panelHardTimeoutMs`)— 已存在于模式和每个组合编辑器中,但全局路由选项卡从未暴露它们,因此在那里选择 "fusion" 实际上是无操作的。这些字段现在被显示(提取到新的 `FusionDefaultsFields` 组件)。投票/聚合模式/每提供者权重有意不显示 — 这些在 fusion 引擎中不存在。回归测试:`tests/unit/ui/combo-defaults-fusion-5598.test.tsx`。([#5598](https://github.com/diegosouzapw/OmniRoute/issues/5598)) - **dashboard(免费代理池):** 免费代理池"全部同步"不再静默失败,显示 `Total: 0`。三个修复:(1) **IPLocate** 源获取 `…/protocols/.json` 并将其解析为 JSON,但上游列表是纯文本(`.txt`,每行一个 `ip:port`)— 每个协议都 404/解析失败;现在获取 `.txt` 并解析行列表。(2) 同步路由**将每个源隔离**在各自的 try/catch 中,因此一个提供者抛出异常(例如 TLS 握手失败)不再中止整个同步 — 工作的源仍填充池。(3) UI 现在**显示路由已返回的每源错误**,而非丢弃响应,因此部分/空同步可以解释自身。回归测试:`tests/unit/free-proxy-providers.test.ts`、`tests/unit/proxy-pool-sync-4878.test.ts`、`tests/unit/free-pool-tab.test.tsx`。([#5595](https://github.com/diegosouzapw/OmniRoute/issues/5595)) - **dashboard(记忆引擎):** 记忆引擎状态页面不再混合英文和葡萄牙语。嵌入/向量存储/重排序**状态详情字符串**在后端被硬编码为葡萄牙语(`resolveEmbeddingSource`、`engineStatus`),例如 `auto: nenhuma fonte de embedding disponível` 和 `sqlite-vec ativo, dim=…`,而周围的 UI 标签从英文 i18n 包渲染 — 因此英文用户看到的是半翻译页面。后端详情字符串现在是英文(`auto: no embedding source available`、`sqlite-vec active, dim=…` 等),与页面其余部分匹配。回归测试:`tests/unit/memory-engine-status.test.ts`。([#5596](https://github.com/diegosouzapw/OmniRoute/issues/5596)) - **providers(cline):** 停止将有效的 **Cline**(OAuth)响应错误映射为 `502 empty_choices` + 账户冷却。`detectMalformedNonStream` 仅识别 `choices[].message.content` 为**字符串**,但一些 OpenAI-compatible 上游 — 通过 OAuth 的 Cline 等 — 在 OpenAI 信封内返回 `content` 作为 **Anthropic 风格文本块数组**。因此非空响应(recvBytes > 0)被分类为 `empty_choices` 并转换为 502,还冷却了账户。格式错误的响应检测器现在也将携带至少一个非空 `text` 块的 content 数组视为真实输出。回归测试:`tests/unit/diagnostics.test.ts`。([#5559](https://github.com/diegosouzapw/OmniRoute/issues/5559)) - **embedded services(Windows):** 修复 **CLIProxyAPI 在 Windows 上立即失败并报 `spawn unzip ENOENT`**。二进制提取器生成了 `unzip`,这不是 Windows 系统命令 — 它仅在 Git for Windows 的 `usr/bin` 中提供,这是 Node 的 `spawn` PATH 永远不会看到的目录,因此即使安装了 Git 的用户也会遇到错误。在 Windows 上,提取器现在使用 PowerShell 内置的 `Expand-Archive`(通过 `execFileAsync`,无 shell — 路径作为单个非解释参数传递,带有 `''`-转义 + `-LiteralPath` 作为纵深防御);其他平台继续使用 `unzip`。这与 #5379 不同(那是 `npm.cmd` 需要 `shell: true`)。回归测试:`tests/unit/binary-manager-extract-zip-5590.test.ts`。([#5590](https://github.com/diegosouzapw/OmniRoute/issues/5590)) - **storage(守护进程):** 修复当 `storage.sqlite` 变得很大(~170 MB+)时启动时的 Node.js **内存不足崩溃**。启动时调用日志清理(`cleanupExpiredLogs` → `rotateCallLogs`)运行了两个**无界 `SELECT … FROM call_logs … .all()`** 查询 — `listReferencedArtifacts`(每个制品路径)和 `deleteCallLogsBefore`(保留截止日期之前的每个 id)。`node:sqlite` 的 `StatementSync.all()` 一次性将整个结果集物化为 JS 对象,因此在大型表上 V8 堆爆了,进程在绑定前崩溃(`FATAL ERROR: … heap out of memory`,原生帧 `node::sqlite::StatementSync::All`)。两个查询现在以有界 5,000 行块分页遍历 `call_logs`(新增 `src/lib/usage/callLogsBoundedQueries.ts`),保持峰值内存不变,无论表大小如何 — 不再需要手动 `--max-old-space-size` 提升。回归测试:`tests/unit/call-log-oom-unbounded-5618.test.ts`。([#5618](https://github.com/diegosouzapw/OmniRoute/issues/5618)) - **dashboard(提供者设置):** 修复三个指向 404 页面的提供者设置链接。**Ollama Cloud** / **ollama-search** 链接到 `ollama.com/settings/api-keys` → 更正为 `ollama.com/settings/keys`(页面已移动;Ollama Cloud 是真实的密钥服务,因此字段保留)。**SearchAPI** 链接到裸 `searchapi.io/docs`(404)→ `searchapi.io/docs/google`。**You.com** 链接到 `you.com/docs/search/overview`(404)→ `you.com/business/api/`(开发者门户)。所有三个替换均经过实时验证。回归测试:`tests/unit/provider-setup-links-5572.test.ts`。([#5572](https://github.com/diegosouzapw/OmniRoute/issues/5572)、[#5574](https://github.com/diegosouzapw/OmniRoute/issues/5574)、[#5576](https://github.com/diegosouzapw/OmniRoute/issues/5576)) - **providers(AI/ML API):** 模型导入步骤现在加载**实时** AI/ML API 目录(400+ 模型),而非回退到过时的 6 模型种子。注册表没有 `modelsUrl`,因此路由静默使用打包目录,即使密钥有效也显示"API 不可用 — 使用本地目录"警告。AI/ML API 在公开、**无需认证**的 `https://api.aimlapi.com/models` 端点暴露其完整目录(裸 `{ id, type, info }` 数组,与 OpenAI-compat `/v1/models` 不同);现在已接入模型路由的发现配置,打包目录保留为离线回退。回归测试:`tests/unit/provider-models-route.test.ts`。([#5570](https://github.com/diegosouzapw/OmniRoute/issues/5570)) - **providers(CablyAI):** 将 **CablyAI** 标记为已弃用 — `cablyai.com` 不再解析(DNS `NXDOMAIN`,2026-06-30 验证);域名已消失。提供者从模型路由发现配置中移除,因此导入步骤返回干净错误而非**未处理的 500 崩溃**(死域名 fetch 抛出异常,无本地目录回退),注册表条目现在携带 `deprecated: true` / `riskNoticeVariant: "deprecated"`,使仪表盘标记现有连接(与已关闭的 `glhf`/`kluster.ai` 网关相同处理)。回归测试:`tests/unit/provider-models-route.test.ts`。([#5568](https://github.com/diegosouzapw/OmniRoute/issues/5568)) - **dashboard(添加提供者):** 非 LLM 搜索/代理提供者不再在模型导入步骤中失败,显示红色 `Provider does not support models listing`。**Jules**(Google Labs 编码代理)、**linkup-search**(Linkup 网络搜索)、**ollama-search**(Ollama Cloud 网络搜索 — 不同于本地 Ollama LLM)和 **searchapi-search**(SearchAPI SERP)没有 `/v1/models` 端点,因此导入将预期行为显示为失败。每个现在提供其可选能力 ID 的小型静态目录 — Linkup 的 `fast`/`standard`/`deep` 搜索深度,SearchAPI 的 `google`/`bing`/`youtube`/… 引擎,单个 Jules/Ollama-web-search 条目 — 因此导入步骤返回可用列表(`source: local_catalog`)而非错误。回归测试:`tests/unit/provider-models-route.test.ts`。([#5569](https://github.com/diegosouzapw/OmniRoute/issues/5569)、[#5571](https://github.com/diegosouzapw/OmniRoute/issues/5571)、[#5573](https://github.com/diegosouzapw/OmniRoute/issues/5573)、[#5575](https://github.com/diegosouzapw/OmniRoute/issues/5575)) - **dashboard(添加提供者):** 没有实时密钥/cookie 验证器的提供者(例如 **LMArena (Free)**、**PiAPI**)现在可以保存。添加连接模态框将后端的 `"Provider validation not supported"` 响应视为硬**无效**状态,完全阻止保存,使这些提供者无法添加。验证路由现在在消息旁边返回 `unsupported: true`,模态框将其视为非阻塞警告 — "检查"徽章仍显示"验证不支持"(信息性),但保存将凭据原样持久化。回归测试:`tests/unit/ui/add-api-key-modal-unsupported-save-5565.test.tsx`(保存继续)和 `tests/unit/providers-validate-route.test.ts`(线路格式)。([#5565](https://github.com/diegosouzapw/OmniRoute/issues/5565)、[#5567](https://github.com/diegosouzapw/OmniRoute/issues/5567)) - **providers(codex):** 修复 **Codex Responses WebSocket** 路径(`/v1/responses`),在 v3.8.40 中回归,出现客户端可见的 `Invalid JSON body` 并绕过配置的代理。(1) #5591 — PR #5237 将模拟 TLS 配置文件提升到 `chrome_149`,但 `wreq-js@2.3.1` 仅支持到 `chrome_147`;未知配置文件产生退化指纹,ChatGPT 拒绝上游升级。Codex WS 路径回退到经过验证的 `chrome_142`(v3.8.39 的值),过度提升的 `grok-web`/`claude-web` 配置文件(被其断路器掩盖但静默丢弃 TLS 模拟)恢复为 `chrome_146`。新增回归测试断言每个配置的 `chrome_*` 配置文件存在于已安装的 `wreq-js` 类型中(`tests/unit/tls-profiles-valid-5591.test.mjs`)。(2) #5611 — 上游 `wreq-js.websocket()` 连接忽略代理注册表,因此无直接出口的 Docker 容器以 DNS 错误失败;准备路由现在解析全局/提供者代理并将其线程传递到 WS 连接。回归测试在 `tests/unit/responses-ws-proxy.test.mjs` 中。([#5591](https://github.com/diegosouzapw/OmniRoute/issues/5591)、[#5611](https://github.com/diegosouzapw/OmniRoute/issues/5611)) - **providers(GLM):** GLM **5.1 / 5.2** 现在保留 `system` 角色,而非将系统提示折叠到第一个用户轮次中。`roleNormalizer.ts` 用 `startsWith("glm")` / `startsWith("glm-")` 前缀匹配每个 `glm*` ID,因此下一代模型 — z.ai 文档化支持 `system` 角色(GLM > 5.0)— 被规范化为好像拒绝它一样,降低了指令遵循能力。匹配器现在是版本感知的:仅对裸 `glm`、4.x 系列和 5.0 代剥离系统角色,为 `glm-5.1`/`glm-5.2`(及 Fireworks `glm-5p1` 点别名)保留它。ZenMux 供应商前缀 `z-ai/glm-*` 压缩历史规则和 ERNIE 规则不变。回归测试在 `tests/unit/role-normalizer.test.ts` 中。([#5610](https://github.com/diegosouzapw/OmniRoute/issues/5610)) - **安全加固后续(v3.8.15):** `auth_token` cookie 现在设置显式 30 天 `maxAge`,使会话按预期持续(Seg3);管理引导在 `INITIAL_PASSWORD` 保留在不安全的 `CHANGEME` 默认值时在启动时警告(Seg2);VS Code 路径令牌端点(`/api/v1/vscode/raw/[token]`)发出每次进程一次的安全警告,因为 API 密钥在 URL 中传输,可能通过日志/代理泄露(Seg4);系统版本路由通过 `npm root -g` 解析真实的全局安装路径,而非硬编码 `/app`(Bug3);自动更新模式检测分段匹配 `node_modules` 而非子字符串匹配,消除误报的"全局安装"(Bug1)。 - **fix(cli):** 将 Node 进程标题重命名为 `omniroute`,使其在 ps/htop 中正确显示。(感谢 @waguriagentic) - **dashboard(模型选择器):** 防范 null 模型别名值,使为自定义提供者节点打开创建组合不再崩溃。`ModelSelectModal` 的自定义提供者分支用原始 `fullModel.startsWith(...)` 过滤 `modelAliases` 条目,当别名值为 `null`/`undefined`(持久化到设置的陈旧/部分条目)时抛出 `TypeError`。过滤/映射逻辑提取到新的 `buildNodeAliasModels` 辅助函数(镜像同级透传别名守卫,#485),在调用 `.startsWith` 前要求 `typeof fullModel === "string"`。回归测试:`tests/unit/model-select-null-alias-guard-2247.test.ts`。(感谢 @wahyuzero) - **fix(translator):** 跨请求格式剥离孤立工具结果(没有匹配工具调用的结果),以避免上游 400 错误。(感谢 @warelik) - **fix(kiro):** 停止在尾随工具结果轮次上注入占位用户轮次,使代理循环不被中断。(感谢 @jetmiky) - **fix(translator):** 防止 OpenAI→Claude 响应中重复的工具参数(重复 finish_reason 守卫 + 字符串工具输入透传)。(感谢 @vishalrajv) - **codex(代理目标流):** 保护长时间运行的代理目标流,使扩展的代理运行不再被过早切断。([#5772](https://github.com/diegosouzapw/OmniRoute/pull/5772) — 感谢 @nguyenxvotanminh3) - **sse(零宽标记):** 从流式响应中剥离零宽标记,与非流式路径匹配,使流式输出达到字节干净的对等性。([#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857) — 感谢 @DKotsyuba) - **usage(om-usage 端点):** 恢复 `om-usage` HTTP 端点。([#5859](https://github.com/diegosouzapw/OmniRoute/pull/5859) — 感谢 @Witroch4) - **sse(流就绪):** 调整自适应流就绪超时,使慢首令牌上游得到更可靠的处理。([#5767](https://github.com/diegosouzapw/OmniRoute/pull/5767) — 感谢 @nguyenxvotanminh3) - **security(提供者节点 URL):** 加固提供者节点 URL 验证。([#5760](https://github.com/diegosouzapw/OmniRoute/pull/5760) — 感谢 @nguyenxvotanminh3) - **cli(Windows 诊断):** 在 Windows 上纠正 `doctor.mjs` 中的 `rootDir` 解析。([#5845](https://github.com/diegosouzapw/OmniRoute/pull/5845) — 感谢 @arssnndr) - **providers(Antigravity):** 修复信用耗尽时的 429 挂起,并应用精确重置时间模型锁定而非停滞 — #5823 的清理重实现。([#5846](https://github.com/diegosouzapw/OmniRoute/pull/5846) — 感谢 @Chewji9875 / @diegosouzapw) - **providers(qwen-web):** 解除验证器和聊天补全的阻塞 — 退役端点已替换,缺失的 SPA 版本请求头现在被发送。([#5855](https://github.com/diegosouzapw/OmniRoute/pull/5855) — 感谢 @janeza2) - **providers(kimi-web):** 在 `kimi.moonshot.cn` 退役后迁移到 `www.kimi.com` Connect-RPC API。([#5858](https://github.com/diegosouzapw/OmniRoute/pull/5858) — 感谢 @janeza2) - **dashboard(CSRF):** 统一仪表盘 CSRF 来源回退,使动态/公共来源正确验证。([#5856](https://github.com/diegosouzapw/OmniRoute/pull/5856) — 感谢 @rdself) - **db(健康检查间隔):** 在连接创建/更新时保留 `healthCheckInterval=0`,而非将其强制转换为默认值。([#5822](https://github.com/diegosouzapw/OmniRoute/pull/5822) — 感谢 @atomlong) - **sse(claude→codex 流式):** 停止 claude→codex 流式上的推理摘要丢失和重复增量 — 推理快照现在在 TRANSLATE 模式下合成,序列号水印按流跟踪(#5786)。([#5832](https://github.com/diegosouzapw/OmniRoute/pull/5832) — 感谢 @diegosouzapw) - **deps(运行时):** 添加缺失的运行时依赖 `@toon-format/toon` 和 `safe-regex`,使发布的包在运行时能够解析它们。([#5771](https://github.com/diegosouzapw/OmniRoute/pull/5771) — 感谢 @chirag127) - **system(Windows 自动更新):** 将应用内自动更新 `npm` 调用通过 win32 shell 辅助程序路由,使更新在 Windows 上正确运行(#5542)。([#5797](https://github.com/diegosouzapw/OmniRoute/pull/5797) — 感谢 @diegosouzapw) - **dashboard(验证徽章):** 为不支持的验证显示中性徽章,使 OAuth 错误消息成为可点击链接(#5442、#5486)。([#5795](https://github.com/diegosouzapw/OmniRoute/pull/5795) — 感谢 @diegosouzapw) - **providers(元数据):** 纠正过时/损坏的提供者元数据(#5487、#5461、#5534、#5470)。([#5790](https://github.com/diegosouzapw/OmniRoute/pull/5790) — 感谢 @diegosouzapw) - **providers(本地目录导入):** 导入有意的仅本地目录提供者,而非显示 502(#5460、#5465)。([#5787](https://github.com/diegosouzapw/OmniRoute/pull/5787) — 感谢 @diegosouzapw) - **proxyfetch(故障转移):** 对不可重放的请求体跳过故障转移重试,使已消费的流不会空着重新发送。([#5770](https://github.com/diegosouzapw/OmniRoute/pull/5770) — 感谢 @Ardem2025) - **batch(恢复):** 在恢复期间持久化批处理项检查点,使中断的批处理从离开的地方恢复。([#5753](https://github.com/diegosouzapw/OmniRoute/pull/5753) — 感谢 @ag-linden) - **memory(Qdrant):** 启用 Qdrant 现在将其激活为检索引擎(`auto` 默认从未选择它)并添加内联指导(#5597)。([#5741](https://github.com/diegosouzapw/OmniRoute/pull/5741) — 感谢 @diegosouzapw) - **chat(非流式聚合):** 加固非流式 SSE 聚合,防范格式错误的上游事件序列。([#5746](https://github.com/diegosouzapw/OmniRoute/pull/5746) — 感谢 @rdself) - **sse(冷却解析):** 防雷鸣群守卫现在容忍数字 epoch 冷却值。([#5747](https://github.com/diegosouzapw/OmniRoute/pull/5747) — 感谢 @diegosouzapw) - **api(请求体大小):** 提高响应路由的 LLM API 有效载荷限制,使更大的请求不被拒绝。([#5652](https://github.com/diegosouzapw/OmniRoute/pull/5652) — 感谢 @JxnLexn) - **providers(HuggingChat):** 修复 HuggingChat 网络会话路由(#5592)。([#5592](https://github.com/diegosouzapw/OmniRoute/pull/5592) — 感谢 @backryun) - **sse(堆压力):** 限制聊天热路径堆 — 压力感知准入、响应上限和克隆减少 — 以避免负载下的 OOM(#5152)。([#5425](https://github.com/diegosouzapw/OmniRoute/pull/5425) — 感谢 @josevictorferreira) - **providers(M365 Copilot):** 验证 M365 Copilot 网络凭据。([#5432](https://github.com/diegosouzapw/OmniRoute/pull/5432) — 感谢 @skyzea1) - **providers(chatgpt-web):** 恢复点形式的 Pro 模型 ID。([#5549](https://github.com/diegosouzapw/OmniRoute/pull/5549) — 感谢 @Thinkscape) - **security(错误堆栈):** 避免在响应中渲染错误堆栈。([#5624](https://github.com/diegosouzapw/OmniRoute/pull/5624) — 感谢 @KooshaPari) - **security(链接化):** 将 `linkifyText` href 限制为显式 `http(s)` 方案白名单。([#948d2d7](https://github.com/diegosouzapw/OmniRoute/commit/948d2d7f2) — 感谢 @diegosouzapw) - **translator(重复工具参数):** 防止 OpenAI→Claude 翻译路径中重复的工具调用参数。([#5828](https://github.com/diegosouzapw/OmniRoute/pull/5828) — 感谢 @diegosouzapw) - **translator(孤立工具结果):** 跨请求格式剥离孤立工具结果轮次,使上游不拒绝没有匹配调用的工具结果。([#5805](https://github.com/diegosouzapw/OmniRoute/pull/5805) — 感谢 @diegosouzapw) - **translator(Gemini/Claude 加固):** 重新应用 Gemini 合并路径和 Claude 工具默认值的丢失防御性加固。([#5706](https://github.com/diegosouzapw/OmniRoute/pull/5706) — 感谢 @diegosouzapw) - **kiro(工具结果轮次):** 停止在工具结果轮次上注入占位用户轮次,这破坏了原本有效的 Kiro 对话。([#5807](https://github.com/diegosouzapw/OmniRoute/pull/5807) — 感谢 @diegosouzapw) - **providers(Kiro 目录):** 将 `claude-sonnet-5` 添加到 Kiro 模型目录。([#5796](https://github.com/diegosouzapw/OmniRoute/pull/5796) — 感谢 @diegosouzapw) - **oauth(连接消歧):** 按用户名消除 OAuth 连接歧义,使两个不同的身份提供者不再相互覆盖。([#5803](https://github.com/diegosouzapw/OmniRoute/pull/5803) — 感谢 @diegosouzapw) - **github(Copilot 预填充):** 为 Copilot 聊天丢弃尾随 assistant 预填充,一些 Copilot 模型会拒绝它。([#5802](https://github.com/diegosouzapw/OmniRoute/pull/5802) — 感谢 @diegosouzapw) - **mitm(hosts 清理):** 在可能时在退出时清理特权 `/etc/hosts` 条目,使崩溃/中断的运行不留下过时的重定向。([#5808](https://github.com/diegosouzapw/OmniRoute/pull/5808) — 感谢 @diegosouzapw) - **dashboard(模型选择器):** 在模型选择器中防范 null `modelAliases` 值,使没有别名的连接不再抛出异常。([#5792](https://github.com/diegosouzapw/OmniRoute/pull/5792) — 感谢 @diegosouzapw) - **dashboard(错误边界):** 为组合和 MITM 代理页面添加错误边界,使渲染错误不再使整个仪表盘空白。([#5788](https://github.com/diegosouzapw/OmniRoute/pull/5788) — 感谢 @diegosouzapw) - **cli(进程标题):** 将运行进程标题重命名为 `omniroute`。([#5791](https://github.com/diegosouzapw/OmniRoute/pull/5791) — 感谢 @diegosouzapw) - **compression(上下文编辑遥测):** 在流式路径上记录上下文编辑遥测,而不仅是非流式路径。([#5761](https://github.com/diegosouzapw/OmniRoute/pull/5761) — 感谢 @diegosouzapw) - **security(v3.8.15 加固后续):** 落地 v3.8.15 安全审查中的 Seg2/Seg3/Seg4/Bug3 加固后续。([#5512](https://github.com/diegosouzapw/OmniRoute/pull/5512) — 感谢 @diegosouzapw) ### 📝 维护 - **docs(架构):** 新增 `docs/architecture/ROUTER_BACKENDS.md` — 一份 ADR,确定路由引擎(`ts` 原生、`bifrost`、`cliproxy`、`9router`、VibeProxy-compatible)如何沿两个正交轴相互关联(生命周期:进程内 / 受监督 / 外部 vs. 中继选择后端),回答 [#5603](https://github.com/diegosouzapw/OmniRoute/issues/5603) 中提出的架构问题(后端接口模型、为什么 CLIProxy 生成进程、功能标志交换、可操作的路由契约错误)。ADR 描述的类型化路由器后端注册表将通过 [#5868](https://github.com/diegosouzapw/OmniRoute/pull/5868) 单独落地。([#5891](https://github.com/diegosouzapw/OmniRoute/pull/5891)) - **tests(autoCombo):** 稳定化 `getTaskFitnessWithSource identifies fitness_table as source for known models` 单元测试,当 models.dev 能力数据库在 CI 中被填充时该测试会 flake:固定模型 `gpt-4o` 是真实的 models.dev 目录 ID,因此适应度解析链返回 `models_dev_tier` 而非预期的静态 `fitness_table` 源。固定装置现在使用 `claude-sonnet`(一个不在 models.dev 目录中的缩短别名,匹配同级解析链测试),确定性地回退到静态表 — 精确的 `source` 和分数断言被保留(`0.95` = `FITNESS_TABLE.coding["claude-sonnet"]`)。([#5890](https://github.com/diegosouzapw/OmniRoute/pull/5890)) — 感谢 @KooshaPari - **oauth(死代码移除):** 删除 `src/lib/oauth/services/` 下被取代的旧版 OAuth **服务类**层次结构。实时 OAuth 流通过 `src/lib/oauth/providers.ts` + `src/lib/oauth/providers/` 运行(接入通用 `oauth/[provider]/[action]` 路由);旧的每提供者 `class *Service extends OAuthService` 实现及其桶有**零**生产或测试引用。移除了 `oauth.ts`(基类)、`openai.ts`、`github.ts`、`claude.ts`、`codex.ts`、`antigravity.ts`、`qwen.ts`、`qoder.ts` 和 `index.ts` 桶(−1559 LOC)。保留了路由**按路径直接**导入的三个仍然活跃的文件:`kiro.ts`(Kiro 导入/交换路由)、`cursor.ts`(Cursor 导入路由)和 `codexImport.ts`(Codex 批量导入路由的实用函数)。由 `typecheck:core` 保持绿色证明安全(任何活跃引用都会使构建失败)+ 文件系统守卫 `tests/unit/oauth-legacy-services-removed.test.ts` 固定移除防止重新引入。关闭的 PR [#5039](https://github.com/diegosouzapw/OmniRoute/pull/5039) 的回收。v3.8.42 差距 — T10(5.7)。 - **refactor(上帝文件分解):** 在 db、sse、usage、api、memory、evals、models、resilience 和 dashboard 上帝文件中提取纯叶子模块(类型/映射器/辅助函数/纯转换叶子;行为保留,测试守卫):db/providers、db/proxies、db/models、db/settings、usageAnalytics、migrationRunner([#5714](https://github.com/diegosouzapw/OmniRoute/pull/5714)、[#5717](https://github.com/diegosouzapw/OmniRoute/pull/5717)、[#5705](https://github.com/diegosouzapw/OmniRoute/pull/5705)、[#5709](https://github.com/diegosouzapw/OmniRoute/pull/5709)、[#5722](https://github.com/diegosouzapw/OmniRoute/pull/5722)、[#5721](https://github.com/diegosouzapw/OmniRoute/pull/5721));sse openai-to-gemini / cursor-protobuf / rate-limit-headers / reasoning-tag([#5824](https://github.com/diegosouzapw/OmniRoute/pull/5824)、[#5794](https://github.com/diegosouzapw/OmniRoute/pull/5794)、[#5736](https://github.com/diegosouzapw/OmniRoute/pull/5736)、[#5734](https://github.com/diegosouzapw/OmniRoute/pull/5734));usage families / callLogs / usageHistory / providerLimits([#5782](https://github.com/diegosouzapw/OmniRoute/pull/5782)、[#5725](https://github.com/diegosouzapw/OmniRoute/pull/5725)、[#5728](https://github.com/diegosouzapw/OmniRoute/pull/5728)、[#5730](https://github.com/diegosouzapw/OmniRoute/pull/5730));api provider-models discovery / unified-catalog([#5758](https://github.com/diegosouzapw/OmniRoute/pull/5758)、[#5699](https://github.com/diegosouzapw/OmniRoute/pull/5699));memory retrieval scoring([#5733](https://github.com/diegosouzapw/OmniRoute/pull/5733));evals golden-set suites([#5740](https://github.com/diegosouzapw/OmniRoute/pull/5740));modelsDevSync transform layer([#5743](https://github.com/diegosouzapw/OmniRoute/pull/5743));resilience settings split([#5745](https://github.com/diegosouzapw/OmniRoute/pull/5745));dashboard sidebarVisibility split([#5683](https://github.com/diegosouzapw/OmniRoute/pull/5683));executor shared-utility dedup + tests(... [truncated]) - **chore(Bun 脚本运行器):** 采用 Bun `1.3.10` 作为锁定、白名单的**构建/开发脚本运行器**,用于一小组经过验证的 TS 门禁/生成器脚本(Node 保持为发布的运行时):锁定的运行时依赖,CI 脚本检查和已验证脚本在 Bun 下运行,以及一个 bun 安全打包验证器。([#5615](https://github.com/diegosouzapw/OmniRoute/pull/5615)、[#5617](https://github.com/diegosouzapw/OmniRoute/pull/5617)、[#5612](https://github.com/diegosouzapw/OmniRoute/pull/5612)、[#5643](https://github.com/diegosouzapw/OmniRoute/pull/5643) — 感谢 @KooshaPari;文档 [#5703](https://github.com/diegosouzapw/OmniRoute/pull/5703) — 感谢 @diegosouzapw) - **docs(同步与整理):** i18n CHANGELOG 镜像同步 [3.8.43] 部分([#5789](https://github.com/diegosouzapw/OmniRoute/pull/5789));MCP 工具计数同步到 95 + 路由策略计数([#5732](https://github.com/diegosouzapw/OmniRoute/pull/5732));README 更快/更精简的安装说明,刷新指标/徽章,17 策略 + 配额共享列表,提供者计数和语法修复([#5713](https://github.com/diegosouzapw/OmniRoute/pull/5713)、[#5738](https://github.com/diegosouzapw/OmniRoute/pull/5738) — 感谢 @chirag127);安全文档:禁止关键词/账户封禁检测([#5756](https://github.com/diegosouzapw/OmniRoute/pull/5756))和完整 LOCAL_ONLY 路由集 + GHSA 公告 + 审计路径([#5748](https://github.com/diegosouzapw/OmniRoute/pull/5748));中继后端路由契约澄清([#5621](https://github.com/diegosouzapw/OmniRoute/pull/5621) — 感谢 @KooshaPari);发布冻结仅限于 `/generate-release`([#5839](https://github.com/diegosouzapw/OmniRoute/pull/5839));`.editorconfig` 仓库标准([#5879](https://github.com/diegosouzapw/OmniRoute/pull/5879) — 感谢 @shiva24082)。— 感谢 @diegosouzapw - **test/ci(稳定化与基准):** 守卫 tsx/esm→esbuild 启动转换([#5773](https://github.com/diegosouzapw/OmniRoute/pull/5773));对齐 t3-web 网络会话元数据([#5835](https://github.com/diegosouzapw/OmniRoute/pull/5835));重新定位侧边栏配额共享位置扫描([#5711](https://github.com/diegosouzapw/OmniRoute/pull/5711));批处理端到端轻量级健康探测([#5651](https://github.com/diegosouzapw/OmniRoute/pull/5651) — 感谢 @KooshaPari);使发布绿色预飞门禁可见且有界([#5644](https://github.com/diegosouzapw/OmniRoute/pull/5644));稳定化夜间变异(tap.testFiles 漂移守卫 + 防 flake eps)([#5682](https://github.com/diegosouzapw/OmniRoute/pull/5682));关闭 QG v2 尾部([#5681](https://github.com/diegosouzapw/OmniRoute/pull/5681));在 Windows 上标准化检查路由路径([#5613](https://github.com/diegosouzapw/OmniRoute/pull/5613) — 感谢 @KooshaPari);将 `sonar.projectVersion` 传递给 SonarQube 扫描([#5880](https://github.com/diegosouzapw/OmniRoute/pull/5880));加上 stryker `tap.testFiles` 注册、压缩工作室冒烟重新锚定、`rtk_discover` 防 flake 以及 v3.8.43 周期基准重新设定(deadExports 225→227、complexity 1981→1982、cognitive-complexity 842→845、eslintWarnings 4121→4158→4199)。— 感谢 @diegosouzapw - **refactor(oauth):** 移除死旧版 OAuth 服务类。([#5838](https://github.com/diegosouzapw/OmniRoute/pull/5838) — 感谢 @diegosouzapw) ### 🙌 贡献者 感谢所有在 v3.8.43 中贡献的各位: | 贡献者 | PR / Issues | | ------------------------------------------------------------ | -------------------------------------------------------------------- | | [@ag-linden](https://github.com/ag-linden) | #5753 | | [@Ardem2025](https://github.com/Ardem2025) | #5770 | | [@arssnndr](https://github.com/arssnndr) | #5845 | | [@atomlong](https://github.com/atomlong) | #5822 | | [@backryun](https://github.com/backryun) | #5592 | | [@baslr](https://github.com/baslr) | 直接提交 / 报告 | | [@Chewji9875](https://github.com/Chewji9875) | #5563, #5579, #5846 | | [@chirag127](https://github.com/chirag127) | #5738, #5771 | | [@DKotsyuba](https://github.com/DKotsyuba) | #5857 | | [@hartmark](https://github.com/hartmark) | #5834 | | [@ishatiwari21](https://github.com/ishatiwari21) | #5799 | | [@janeza2](https://github.com/janeza2) | #5855, #5858 | | [@jetmiky](https://github.com/jetmiky) | 直接提交 / 报告 | | [@josevictorferreira](https://github.com/josevictorferreira) | #5425 | | [@JxnLexn](https://github.com/JxnLexn) | #5652 | | [@KooshaPari](https://github.com/KooshaPari) | #5613, #5621, #5624, #5629, #5643, #5651, #5890 | | [@KunN-21](https://github.com/KunN-21) | 直接提交 / 报告 | | [@manhdzzz](https://github.com/manhdzzz) | 直接提交 / 报告 | | [@nguyenxvotanminh3](https://github.com/nguyenxvotanminh3) | #5760, #5767, #5772 | | [@noir017](https://github.com/noir017) | 直接提交 / 报告 | | [@pizzav-xyz](https://github.com/pizzav-xyz) | #5720 | | [@rdself](https://github.com/rdself) | #5746, #5856 | | [@shiva24082](https://github.com/shiva24082) | #5879 | | [@skyzea1](https://github.com/skyzea1) | #5432, #5701 | | [@Stazyu](https://github.com/Stazyu) | #5557 | | [@Thinkscape](https://github.com/Thinkscape) | #5549 | | [@vishalrajv](https://github.com/vishalrajv) | 直接提交 / 报告 | | [@voravitl](https://github.com/voravitl) | 直接提交 / 报告 | | [@waguriagentic](https://github.com/waguriagentic) | 直接提交 / 报告 | | [@wahyuzero](https://github.com/wahyuzero) | 直接提交 / 报告 | | [@warelik](https://github.com/warelik) | 直接提交 / 报告 | | [@Witroch4](https://github.com/Witroch4) | #5731, #5859, #5863 | | [@diegosouzapw](https://github.com/diegosouzapw) | 维护者 — 周期协调、发布关闭基础红色修复、上帝文件分解、压缩/记忆功能 | --- ## [3.8.42] — 2026-06-30 ### ✨ 新功能 - **compression (pipeline):** 为级联压缩管线(T02 / Headroom H1)新增默认启用的**膨胀防护**。如果完全级联的引擎产生的输出实际上没有缩小——其 Token 数量 `>=` 原始值——则丢弃压缩结果,改为向上游发送原始请求,并在压缩统计中记录 `pipeline-inflation-guard` 警告。这通过构造保证了安全(唯一的回退是未修改的原始请求,始终是有效载荷),并补充了现有的可选每步 TV1 退出机制(后者控制步骤间的推进而非最终输出)。新增 `open-sse/services/compression/pipelineGuards.ts`;在同步和异步级联路径共享的单一 `finalizeStackedResult` 关键点接入。回归测试(含膨胀引擎集成测试)在 `tests/unit/compression-pipeline-inflation-guard.test.ts`。 - **compression (caveman):** 补齐德语、法语和日语规则包的 `dedup`(重复上下文折叠)和 `ultra`(缩写/精简)类别——这三种语言之前仅提供 `context`/`filler`/`structural`,而 `en`/`es`/`id`/`pt-BR` 拥有全部五种。因此以更高强度压缩的 de/fr/ja 对话现在会折叠重复的样板文本("wie bereits besprochen" → "Siehe oben."、"comme mentionné précédemment" → "Voir ci-dessus."、"前述のとおり" → "(上記参照)"),并缩写密集的技术词汇(`Datenbank`→`DB`、`Authentifizierung`→`Auth`;`base de données`→`BD`、`authentification`→`auth`;`データベース`→`DB`、`アプリケーション`→`app`)。模式与现有的 `es` 包一致,并保持 ReDoS 安全(有界字面量交替;CJK 包不使用 `\b`,因为日语没有词边界)。回归测试:`tests/unit/caveman-packs-de-fr-ja.test.ts`(包加载 + 验证 + 压缩代表性样本)。gaps v3.8.42 — T05/C2。 - **compression (caveman):** 新增**中文(zh / wenyan 文言)输入侧规则包**——对应已有的输出侧 `terse-cjk` 风格。新增 `rules/zh/{dedup,filler,ultra}.json`,折叠重复上下文("如前所述" → "见上。")、删除客套/犹豫用语("请帮我…/谢谢/我觉得")、去除句末语气助词("吗/呢/吧"),并缩写密集技术术语("数据库"→"DB"、"应用程序"→"app")。中文现已支持自动检测:`detectCompressionLanguage` 通过"含汉字但不含假名"来区分 zh 和 ja(假名是日语专属,因此含大量汉字的日语语句仍会解析为 `ja`),`zh` 已列入 `listSupportedCompressionLanguages`。模式保持 ReDoS 安全(有界字面量交替,CJK 无语边界因此不使用 `\b`)。回归测试:`tests/unit/caveman-packs-zh-wenyan.test.ts`(包加载 + 验证 + 压缩;zh/ja/非 CJK 检测)。gaps v3.8.42 — T05/C6。 - **compression (RTK):** 将 **Gradle** 和 **.NET CLI(`dotnet`)** 添加到 RTK 工具输出过滤器目录。`gradle`/`gradlew` 和 `dotnet build|test|restore|publish` 的工具输出现在可被识别(通过命令和输出内容双重方式)并压缩:Gradle 守护进程/欢迎横幅和无效 `> Task … UP-TO-DATE/SKIPPED/FROM-CACHE` 行被丢弃,而 `BUILD SUCCESSFUL/FAILED`、"What went wrong" 和堆栈跟踪被保留;.NET 构建横幅、版权声明以及 `Determining projects to restore`/`Restored …` 等杂项被丢弃,而 `Build succeeded/FAILED`、`error CS####`/`warning CS####` 和测试摘要被保留。新增内置过滤器 `engines/rtk/filters/{gradle,dotnet}.json`(含由目录门控运行的内联测试),以及命令检测器中的 `gradle`/`dotnet` 条目。回归测试:`tests/unit/rtk-gradle-dotnet-filters.test.ts`。gaps v3.8.42 — T07/R9。 ### 🔧 问题修复 - **providers (chatgpt-web):** 修复在 **Electron 桌面应用**上出现的 `502 ChatGPT sentinel failed: Digest method not supported`,该错误导致所有 `chatgpt-web/*` 请求失败。哨兵工作量证明原本使用原生 `createHash("sha3-512")` 进行哈希,但 Electron 的 Node 是基于 **BoringSSL 构建的,而 BoringSSL 未实现 SHA-3 系列算法**(electron/electron#30530),因此摘要计算在构造阶段就抛出异常——该服务商在桌面版本中完全不可用(在纯 Node/OpenSSL 环境下正常工作)。PoW 现在通过一个新的运行时可移植辅助函数(`open-sse/utils/sha3-512.ts`)来计算哈希,优先使用原生摘要,当原生 SHA-3 不可用时透明回退到无依赖的纯 JS Keccak-f[1600] 实现。回退方案已针对原生 `createHash("sha3-512")`(300 个随机输入)和已发布的 FIPS-202 已知答案向量进行了逐位验证。回归测试:`tests/unit/chatgpt-web-sha3-boringssl-5531.test.ts`。([#5531](https://github.com/diegosouzapw/OmniRoute/issues/5531)) - **providers (bytez):** 修复 Bytez 密钥校验("Provider validation endpoint not supported")及聊天基础 URL,已使用真实密钥在线验证。Bytez **确实**是 OpenAI 兼容的(基础路径为 `…/models/v2/openai/v1`),但注册表中存储的是裸的 `…/models/v2` 路径,导致校验对话探测请求访问了 `…/models/v2/chat/completions` → `404` → 产生误导性的"不支持端点"错误。修复分两部分:(1) 注册表中的 `baseUrl` 现在携带完整的 OpenAI 兼容聊天路径(`…/models/v2/openai/v1/chat/completions`);(2) 密钥校验不再使用对话探测——Bytez 账户仅提供显式添加到其目录中的模型,因此即使有效的密钥对任何模型 ID 都会返回 404。改为通过专门的 `validateBytezProvider` 探测**仅用于认证的** `GET …/models/v2/list/tasks` 端点(`200` ⇒ 有效,`401/403` ⇒ 无效),该端点独立于目录配置。回归测试:`tests/unit/bytez-validation-5422.test.ts`。([#5422](https://github.com/diegosouzapw/OmniRoute/issues/5422)) - **dashboard (provider add):** 两项添加服务商 UI 修复。(1) #5420 — "导入模型"按钮现在也对**工具专用服务商**(网页搜索 / 网页抓取)保持隐藏,而不仅仅是 `*-search` ID:`firecrawl` 和 `jina-reader`(声明了 `serviceKinds: ["webFetch"]`)之前会显示导入按钮,点击后会触发 `400 "does not support models listing"` 路由。新增能力检查(`providerLacksModelListing`,基于已解析的 serviceKinds)来控制该区域,不会隐藏 LLM/媒体服务商。(2) #5426 — Coze 密钥校验不再向 UI 泄露原始上游响应包(`{code,msg,logId,from}`);Coze 格式的错误现在转换为友好的 `Coze rejected the key: (code )` 消息(限定在 `provider === "coze"` 范围内,不影响其他服务商)。回归测试:`tests/unit/model-listing-capability-5420.test.ts`、`tests/unit/coze-validation-error-5426.test.ts`。([#5420](https://github.com/diegosouzapw/OmniRoute/issues/5420), [#5426](https://github.com/diegosouzapw/OmniRoute/issues/5426)) - **providers (friendliai, novita):** 修复两个拒绝有效密钥的服务商注册表端点(已使用真实密钥在线验证)。**FriendliAI** 指向 `…/dedicated/v1/chat/completions`,这会对无服务器 `flp_*` Token 返回 `403 Forbidden`——已切换到 `…/serverless/v1/chat/completions`(+ 无服务器 `modelsUrl`)。**Novita** 指向旧的 `…/v3/…` 基础路径,且模型 ID 存在拼写错误 `ai-ai/llama-3.1-8b-instruct`(两者均 `404`)——已切换到 OpenAI 兼容的 `…/openai/v1/…` 基础路径 + 正确的 `meta-llama/llama-3.1-8b-instruct` ID。回归测试:`tests/unit/provider-endpoints-friendliai-novita.test.ts`。([#5430](https://github.com/diegosouzapw/OmniRoute/issues/5430), [#5455](https://github.com/diegosouzapw/OmniRoute/issues/5455)) - **providers (muse-spark):** 将 Muse Spark Web(Meta AI)Cookie 文案与实际 Cookie 名称对齐。默认会话 Cookie 已从已退役的 `abra_sess` 迁移到 `ecto_1_sess`(`META_AI_DEFAULT_COOKIE`),但服务商表单提示和一条 401 认证失败消息仍然提示用户粘贴 `abra_sess`——一个已不存在的 Cookie。两处字符串现在均使用 `ecto_1_sess`。回归测试:`tests/unit/muse-spark-cookie-copy-5449.test.ts`。([#5449](https://github.com/diegosouzapw/OmniRoute/issues/5449)) - **dashboard (provider add):** 修复在整个服务商目录审计中报告的三项"添加 API 密钥"/模型导入流程的粗糙之处。(1) **校验模型**和**账户 ID** 表单字段使用了未翻译的 i18n 占位文案(`"Validation Model Id Label"`、`"Account Id Placeholder"` 等),这些文案直接显示在模态框中——已替换为 `en.json` 中的真实标签/占位符/提示。(2) 模型导入**静默回退到缓存/本地目录**:路由已经返回了 `warning`("API unavailable — using local catalog"),但 `useModelImportHandlers` 只读取 `models`/`error` 而丢弃了它,因此用户获得本地模型却没有任何提示——警告现在以导入日志行的形式呈现(新增纯辅助函数 `extractImportWarning`)。(3) 必填的连接**名称**字段默认值为 `""`,导致浏览器自动填充可能注入垃圾数据(例如 `wiw`)——现在默认为 `"main"`。回归测试:`tests/unit/provider-add-ux-i18n-import-warning.test.ts`。([#5421](https://github.com/diegosouzapw/OmniRoute/issues/5421), [#5428](https://github.com/diegosouzapw/OmniRoute/issues/5428), [#5429](https://github.com/diegosouzapw/OmniRoute/issues/5429), [#5431](https://github.com/diegosouzapw/OmniRoute/issues/5431), [#5435](https://github.com/diegosouzapw/OmniRoute/issues/5435)) - **services (installer):** 修复在 **Windows + Node.js 24+** 上安装内嵌服务(9Router / CLIProxy)时出现的 `spawn EINVAL`。Node 24 不再允许 `child_process.execFile()` 在没有 Shell 的情况下运行 `.cmd` 批处理文件(nodejs/node#52554),而 Windows 上的 npm 是 `npm.cmd`,因此用户点击**安装**时 `runNpm()` 立即抛出 `EINVAL`。`runNpm` 现在仅在 win32 上启用 `shell`。为了在第 13 号硬规则下安全启用 Shell(Shell 而非 `execFile` 来解析 argv),安装 `--prefix`(`DATA_DIR` 路径,可能包含空格,例如 `C:\Users\John Doe\.omniroute\…`)现在通过 `npm_config_prefix` **环境变量**传递,代替 argv 路径参数,并且用户提供的安装 `version` 在路由边界处受限于 dist-tag/semver 格式(`SERVICE_VERSION_PATTERN`),因此永远不会携带 Shell 元字符。prefix 在环境变量中、version 经过校验,其余每个 argv 条目都是静态标志。回归测试:`tests/unit/services/installers/runNpm-shell-5379.test.ts`(+ 现有的 `ninerouter.test.ts` 已对齐到 npm 的 `npm_config_prefix` 环境变量)。([#5379](https://github.com/diegosouzapw/OmniRoute/issues/5379)) - **cli (serve):** 恢复 `dist/tls-options.mjs` 到 npm 包中——可选的原生 HTTPS/TLS 边车(#5361)在构建时被复制到暂存区 `dist/` 中,但随后在预发布白名单步骤中被**裁剪移除**,导致 `omniroute serve` 在已发布的 3.8.41 版本上因 `ERR_MODULE_NOT_FOUND` 崩溃(`dist/server-ws.mjs` 导入了 `./tls-options.mjs`)。已将 `tls-options.mjs` 添加到 `APP_STAGING_ALLOWED_EXACT_PATHS`(通过裁剪检查)和 `dist/tls-options.mjs` 添加到 `PACK_ARTIFACT_REQUIRED_PATHS`(`check:pack-artifact` 门控现在如果该文件再次消失会响亮失败——与 `webdav-handler.mjs` 使用相同的防护模式)。回归测试:`tests/unit/pack-artifact-policy.test.ts`。([#5452](https://github.com/diegosouzapw/OmniRoute/issues/5452) — 感谢 @KooshaPari 的并行修复 [#5494](https://github.com/diegosouzapw/OmniRoute/pull/5494)) - **dashboard:** 修复**添加服务商/引导向导**按钮点击后无反应的 bug。`/dashboard/providers/new` 路由是一个重定向桩(直接跳回 `/dashboard/providers`),因此每个"添加服务商"按钮和仪表盘小部件链接都打开了空白页,完整构建的 `ProviderOnboardingWizard` 组件被孤立(从未被任何路由渲染)。该路由现在直接渲染向导;认证由 `(dashboard)` 布局集中执行,与其他同级服务商路由相同。回归测试:`tests/unit/onboarding-wizard-route-5427.test.ts`。([#5427](https://github.com/diegosouzapw/OmniRoute/issues/5427)) - **db (import):** 修复在 **Windows** 上导入数据库时出现的 `EBUSY: resource busy or locked`。导入路由在 `resetDbInstance()` 之后立即使用普通 `fs.unlinkSync` 删除活动的 `storage.sqlite` + WAL/`-shm`/`-journal` 边车文件,但 Windows 在 `close()` 之后异步释放 SQLite 文件句柄(mmap / 杀毒软件),导致 unlink 竞态并抛出 `EBUSY`。路由现在通过 `unlinkFileWithRetry`(EBUSY/EPERM 退避)来删除——与恢复路径已经使用的辅助函数相同。回归测试:`tests/unit/db-import-ebusy-5406.test.ts`。([#5406](https://github.com/diegosouzapw/OmniRoute/issues/5406),合并至 [#5161](https://github.com/diegosouzapw/OmniRoute/issues/5161)) - **build:** 将 `ioredis` 排除在客户端/CLI 打包之外——一项 dast-smoke 回归测试发现该模块被引入浏览器/Electron 客户端侧代码块;将其添加到 `SPAWN_CAPABLE_PREFIXES` 叶子节点可在服务端路径上保持可用,同时从客户端打包中排除。([#5546](https://github.com/diegosouzapw/OmniRoute/pull/5546)) - **providers (mimocode):** 通过 SOCKS5 代理调度器路由每个账户的流量——每个 mimocode 账户的请求现在通过其配置的 SOCKS5 代理而非默认直连来发送。([#5521](https://github.com/diegosouzapw/OmniRoute/pull/5521) — 感谢 @pizzav-xyz) - **providers:** 在页面刷新时持久化**已配置**服务商过滤器选项——之前的过滤器在每次导航时都会重置为"All"。([#5510](https://github.com/diegosouzapw/OmniRoute/pull/5510) — 感谢 @KooshaPari) - **providers (chatgpt-web):** 支持 **GPT-5.5 Pro** 模型交接——增加了 GPT-5.5 Pro 层级所需的模型映射和交接路由。([#5536](https://github.com/diegosouzapw/OmniRoute/pull/5536) — 感谢 @Thinkscape) - **dashboard:** 保持引导 Schema 的浏览器安全性——Schema 模块导入了服务端的 `db` 引用,导致浏览器打包崩溃;现在仅在服务端路径上导入。([#5525](https://github.com/diegosouzapw/OmniRoute/pull/5525) — 感谢 @KooshaPari) - **routing (bifrost):** 为 bifrost 目标添加自动容灾冷却期——在冷却窗口内防止快速重新选择失败的 bifrost 后端,与现有的熔断器机制互补。([#5519](https://github.com/diegosouzapw/OmniRoute/pull/5519) — 感谢 @KooshaPari) - **providers (opencode-plugin):** 将 opencode 插件升级到 **v0.2.0** 并在发布时接入自动发布功能,使插件包自动跟踪 OmniRoute 发布节奏。([#5363](https://github.com/diegosouzapw/OmniRoute/pull/5363) — 感谢 @herjarsa) - **rate-limit:** 规范化队列刷新设置——在各速率限制策略间对齐队列刷新间隔配置,使过期队列按一致的节奏释放。([#5499](https://github.com/diegosouzapw/OmniRoute/pull/5499) — 感谢 @KooshaPari) - **fallback:** 规范化服务商错误规则头提取——确保容灾重试决策能正确读取所有响应头(不区分大小写),修复服务商的 `Retry-After` 或自定义错误头被静默丢弃的问题。([#5473](https://github.com/diegosouzapw/OmniRoute/pull/5473) — 感谢 @KooshaPari) - **routing:** 将 Claude 自适应思考默认值放在功能标志之后——防止思考预算被注入到不支持扩展思考参数的模型请求中,避免对非思考型 Claude 变体产生上游 `400` 错误。([#5480](https://github.com/diegosouzapw/OmniRoute/pull/5480) — 感谢 @KooshaPari) - **ci:** 修复由死代码清理引入的合并后 CI 回归——恢复了在 ratchet 先于下游消费者登陆时损坏的测试导入和类型引用。([#5467](https://github.com/diegosouzapw/OmniRoute/pull/5467) — 感谢 @KooshaPari) - **sse:** 将终端流取消视为完成——被中止的 SSE 流会被留在部分状态,导致下游消费者无限等待一个永远不会到达的最终事件。([#5491](https://github.com/diegosouzapw/OmniRoute/pull/5491) — 感谢 @JxnLexn) - **api:** 修复非流式 JSON 响应的帧封装——`stream: false` 的聊天补全响应返回时缺少正确的 content-length 帧封装,导致某些客户端错误解析响应体。([#5416](https://github.com/diegosouzapw/OmniRoute/pull/5416) — 感谢 @rdself) - **dashboard (tests):** 使用 CSRF 校验保护动态仪表盘端点测试——测试套件在没有 CSRF Token 的情况下测试仪表盘 API 路由,掩盖了这些端点的覆盖率缺口。([#5405](https://github.com/diegosouzapw/OmniRoute/pull/5405) — 感谢 @rdself) - **providers:** 移除已关闭的 **Phind** 服务商(服务已停止运营),并清理 **HuggingChat** 目录列表中累积的过期重复条目。([#5530](https://github.com/diegosouzapw/OmniRoute/pull/5530) — 感谢 @backryun) - **providers (longcat):** 更正 LongCat 免费层——**LongCat-2.0** 现已正式发布;一次性 1000 万 Token 推广活动(需 KYC)已在目录中正确反映,替换了过时的旧版测试条目。([#5508](https://github.com/diegosouzapw/OmniRoute/pull/5508) — 感谢 @backryun) ### 📝 维护 - **dashboard (refactor):** 将压缩设置标签页中重复的 caveman 开关整合到单一来源面板 (T11),消除过时的不同步副本。([#5524](https://github.com/diegosouzapw/OmniRoute/pull/5524)) - **tests:** 添加 Claude-Code 身份版本同步的配额守护(第二阶段)——断言配额记账中报告的 Claude-Code 版本与已部署版本保持同步,防止静默漂移。([#5514](https://github.com/diegosouzapw/OmniRoute/pull/5514)) - **docs:** 添加中继后端策略指南,文档化支持的中继后端类型、选择标准和配置模式。([#5547](https://github.com/diegosouzapw/OmniRoute/pull/5547)) - **docs:** 说明 bifrost 中继后端环境变量——文档化哪些环境变量控制 bifrost 的中继后端选择和容灾行为。([#5520](https://github.com/diegosouzapw/OmniRoute/pull/5520) — 感谢 @KooshaPari) - **tests:** 添加中继路由容灾头行为测试——回归防护,断言容灾触发的中继请求通过路由层携带正确的转发头。([#5526](https://github.com/diegosouzapw/OmniRoute/pull/5526) — 感谢 @KooshaPari) - **ci:** 添加 npm `fetch-retry` 配置并规范化发布冻结协议(第 21 号硬规则)——减少 CI 中瞬态的 npm 注册表拉取失败,并建立文档化的发布冻结流程。([#5506](https://github.com/diegosouzapw/OmniRoute/pull/5506)) - **deps:** 将 11 个生产依赖升级到最新兼容版本。([#5414](https://github.com/diegosouzapw/OmniRoute/pull/5414)) - **deps:** 将 `/electron` 中的 Electron 从 42.4.1 升级到 42.5.1。([#5413](https://github.com/diegosouzapw/OmniRoute/pull/5413)) - **deps:** 将开发依赖组升级,共 9 项更新。([#5415](https://github.com/diegosouzapw/OmniRoute/pull/5415)) - **maintenance (dead-code):** 全仓库清理未使用的导出符号、类型和 Schema — 移除了 cloud-agent、a2a、SSE、memory、quota、skills、gamification、codex、qdrant、playground、服务商目录和 Combo 模块中 35 个不再被引用的导出,减少了导出 API 面并消除了过时误导性类型。([#5372](https://github.com/diegosouzapw/OmniRoute/pull/5372), [#5373](https://github.com/diegosouzapw/OmniRoute/pull/5373), [#5374](https://github.com/diegosouzapw/OmniRoute/pull/5374), [#5375](https://github.com/diegosouzapw/OmniRoute/pull/5375), [#5376](https://github.com/diegosouzapw/OmniRoute/pull/5376), [#5377](https://github.com/diegosouzapw/OmniRoute/pull/5377), [#5378](https://github.com/diegosouzapw/OmniRoute/pull/5378), [#5380](https://github.com/diegosouzapw/OmniRoute/pull/5380), [#5381](https://github.com/diegosouzapw/OmniRoute/pull/5381), [#5382](https://github.com/diegosouzapw/OmniRoute/pull/5382), [#5383](https://github.com/diegosouzapw/OmniRoute/pull/5383), [#5384](https://github.com/diegosouzapw/OmniRoute/pull/5384), [#5385](https://github.com/diegosouzapw/OmniRoute/pull/5385), [#5386](https://github.com/diegosouzapw/OmniRoute/pull/5386), [#5387](https://github.com/diegosouzapw/OmniRoute/pull/5387), [#5388](https://github.com/diegosouzapw/OmniRoute/pull/5388), [#5389](https://github.com/diegosouzapw/OmniRoute/pull/5389), [#5390](https://github.com/diegosouzapw/OmniRoute/pull/5390), [#5391](https://github.com/diegosouzapw/OmniRoute/pull/5391), [#5392](https://github.com/diegosouzapw/OmniRoute/pull/5392), [#5393](https://github.com/diegosouzapw/OmniRoute/pull/5393), [#5395](https://github.com/diegosouzapw/OmniRoute/pull/5395), [#5396](https://github.com/diegosouzapw/OmniRoute/pull/5396), [#5397](https://github.com/diegosouzapw/OmniRoute/pull/5397), [#5398](https://github.com/diegosouzapw/OmniRoute/pull/5398), [#5399](https://github.com/diegosouzapw/OmniRoute/pull/5399), [#5400](https://github.com/diegosouzapw/OmniRoute/pull/5400) — 感谢 @JxnLexn) - **maintenance (DRY):** 共享辅助函数的 DRY 整合 — 将 17 个重复的工具函数提取到单一共享模块中:vscode 元数据辅助函数、代理路由处理器、auth zip 提取器、Combo 构建器模型选项、vscode Token 化请求辅助函数、配额策略排序辅助函数、recharts 环形图卡片、服务商特定校验、批次响应格式化器、Redis 运行时辅助函数、版本管理器请求解析、媒体生成路由辅助函数、服务安装辅助函数、设置转换 Schema、中继流终结器、machine-id 回退和 node SQLite 适配器。([#5471](https://github.com/diegosouzapw/OmniRoute/pull/5471), [#5472](https://github.com/diegosouzapw/OmniRoute/pull/5472), [#5475](https://github.com/diegosouzapw/OmniRoute/pull/5475), [#5477](https://github.com/diegosouzapw/OmniRoute/pull/5477), [#5479](https://github.com/diegosouzapw/OmniRoute/pull/5479), [#5482](https://github.com/diegosouzapw/OmniRoute/pull/5482), [#5484](https://github.com/diegosouzapw/OmniRoute/pull/5484), [#5485](https://github.com/diegosouzapw/OmniRoute/pull/5485), [#5488](https://github.com/diegosouzapw/OmniRoute/pull/5488), [#5490](https://github.com/diegosouzapw/OmniRoute/pull/5490), [#5492](https://github.com/diegosouzapw/OmniRoute/pull/5492), [#5493](https://github.com/diegosouzapw/OmniRoute/pull/5493), [#5495](https://github.com/diegosouzapw/OmniRoute/pull/5495), [#5496](https://github.com/diegosouzapw/OmniRoute/pull/5496), [#5497](https://github.com/diegosouzapw/OmniRoute/pull/5497), [#5498](https://github.com/diegosouzapw/OmniRoute/pull/5498), [#5500](https://github.com/diegosouzapw/OmniRoute/pull/5500) — 感谢 @JxnLexn) --- ## [3.8.41] — 2026-06-29 ### ✨ 新功能 - **feat(relay): 可选择的中继后端(TS / Bifrost / `auto`)** — OpenAI 兼容的中继端点现在可以通过原生 Bifrost 边车路由其热路径,客户端无需更改 URL。`OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`:默认使用现有的 TypeScript 中继;`auto` 在 `BIFROST_BASE_URL` 已设置(且 `BIFROST_ENABLED` ≠ `0`)时选择 Bifrost,并在边车不可达时自动回退到 TS;`bifrost` 保持严格的失败行为。认证、每 IP/Token 速率限制、提示注入检查和模型白名单仍在 Next 中继路由中于调度前运行(控制平面保留在应用内);响应携带 `X-Routing-Backend` / `X-Routing-Fallback`。回归测试:`tests/unit/api/v1/relay-routing-backend.test.ts`、`tests/unit/api/v1/bifrost-sidecar.test.ts`。([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — 感谢 @KooshaPari) ### 🔧 问题修复 - **translator (claude):** 当 OpenAI→Claude 请求**仅**包含 `system`/`developer` 消息时,合成一个最小化的 `user` 轮次,使请求不再因 `[400]: messages: at least one message is required` 而失败。`openaiToClaudeRequest` 将所有 system/developer 轮次提升到 Claude 的顶层 `system` 字段并从 `messages` 中过滤掉;纯 system 输入(OpenCode 压缩/标题生成请求)会导致 `messages: []`,Messages API 会拒绝——在 OpenCode 中表现为丢失对话的任务中途 `stream error`。该防护仅在 `messages` 原本为空时触发(system 指令仍然驱动响应),因此非空请求不受影响。([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — 感谢 @wild-feather) - **providers (gemini):** 移除已退役的 Google AI Studio 模型 ID,并使目录与实际运行的 GenAI API 对齐(2026-06-29 根据官方弃用页面验证)。移除长期退役的 `gemini-1.5-pro`/`gemini-1.5-flash`、已关闭的 `gemini-2.0-flash`/`gemini-2.0-flash-lite` 和无效实验版;将 `gemini-3.1-flash-lite-preview` 重命名为 GA 版 `gemini-3.1-flash-lite`;将退役的 `text-embedding-004` 替换为实际运行中的 `gemini-embedding-001`/`gemini-embedding-2`;并添加优雅的 `modelDeprecation` 转发,使旧版/重命名的 ID 重定向到 GA 模型而非 404。原生 AI-Studio 直连的图像/视频/音乐注册有意不在本次范围内(需要实际的执行器工作;这些模型仍可通过 Antigravity/Vertex/聚合器访问)。([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — 感谢 @backryun) - **services (dashboard):** 修复内嵌服务仪表盘故障 (#5298) — 服务监控器现在从 `/api/services/[name]/logs` 延迟初始化,因此 `cliproxy`/`9router` 日志在引导注册监控器之前不再 404;生命周期按钮发送 JSON(空安装体默认 `version: "latest"`,格式错误的 JSON 仍返回 `400 Invalid JSON body`);生命周期和日志流故障现在以可操作的 UI 错误呈现,而非静默显示无日志;Tailscale CGNAT `100.64.0.0/10` 对等节点被视为私有 LAN 本地网络,用于仅限本地的服务访问;父路由 `/dashboard/context` → `/dashboard/context/settings` 重定向停止 RSC 预取 404;`/api/v1/providers/{cliproxyapi,9router}/models` 返回同步的内嵌服务模型而非 `invalid_provider`。([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — 感谢 @KooshaPari) - **thinking (claude):** 修复 OpenAI 兼容路径(Cursor → Claude OAuth)上 Claude 自适应思考的三个独立缺陷。**(A)** 仪表盘思考预算设置在每次重启时被丢弃——`setThinkingBudgetConfig` 从未在启动时调用,因此保存的 `{mode:"adaptive"…}` 静默回退到透传模式;现在在 `server-init` 中从设置中恢复。**(B)** Claude 执行器在翻译_之后_强制注入自适应思考,忽略了运维人员的预算——现在遵循 `mode:"auto"`(剥离),同时保持默认(透传)行为逐字节不变,因此原生 Claude Code 不受影响,并将运维人员的 `thinking.type:"enabled"` 重新映射为 Opus 4.7/4.8 所需的 `adaptive` 形状(`enabled` → 400)。**(D)** 在回放时,无签名的 `reasoning_content` 被重建为携带伪造签名的 `thinking` 块 → Anthropic `400 "Invalid signature in thinking block"`;现在发出无签名的 `redacted_thinking` 块(真实签名仍然逐字保留)。回归测试:`tests/unit/thinking-budget-hydration-5312.test.ts`、`base-thinking-budget-config-5312.test.ts`、`openai-to-claude-redacted-replay-5312.test.ts`(现有 #5123/#4479/#2454 套件仍通过)。`` 内容标记通道不匹配(RC-C,与 #5245 共享)作为后续任务待 Anthropic 在线验证。([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — 感谢 @vitalNohj) - **opencode (proxy pool):** OpenCode Free 每账户代理模态框现在提供全局**代理池下拉菜单**(按 ID 引用),而不是强制在每个账户上手动填写 Host/Port/凭证——#5217 缺口 1。**已保存/自定义**切换开关:"已保存"从 `GET /api/settings/proxies` 中选择预保存的代理并存储 `{fingerprint, proxyId}`,因此更新该池代理会应用到使用它的所有账户;"自定义"保留手动输入(内联存储)作为退路。解析在服务端进行(`resolveAccountProxiesFromRegistry`),因此执行器仍然收到不变的已解析代理;现有的内联条目继续有效,未知/已删除的 `proxyId` 安全降级为直连。回归测试:`tests/unit/noauth-proxy-resolution.test.ts`、`tests/unit/ui/noauth-account-card.test.tsx`。([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) 缺口 1 — 感谢 @daniij) - **thinking (claude):** 让原生 reasoning_content 客户端(如 Cursor)可以退出 `` 关闭标记,使其不再将孤立的 `` 泄漏到可见的 `content` 中(#5312 的 RC-C,与 #5245 共享)。标记抑制机制已存在(UA 白名单,#5348),但 Cursor 的 UA 被有意排除在外;此次添加了显式请求头 `x-omniroute-thinking-marker: off`(也支持 `on`/`keep` 强制保留)来覆盖 UA 策略。未携带此头时行为逐字节不变——扫描 `content` 中标记的 Claude Code/Cursor-composer 客户端(#4633)仍然会收到它。回归测试:`tests/unit/think-close-marker-suppress-5245.test.ts`(#5123 case-b + #4479 仍通过)。([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — 感谢 @vitalNohj, @wild-feather) - **cors:** 浏览器/Electron 客户端(如 Wayland AI)现在可以开箱即用地将 OmniRoute 作为 OpenAI 兼容的服务商使用。Token 认证的 API 面(`/v1/*`、`/v1beta/*`)现在默认返回宽松的 `Access-Control-Allow-Origin`(回显请求中的 `Origin`,无时使用 `*`)——与 9router 和 OpenAI 兼容生态系统保持一致——因此渲染进程的 `fetch` 可以读取响应,而不是因 CORS 阻止而失败为"站点未找到"/空目录(而 `curl` 不发送预检请求,可以正常工作)。这是**安全的**:这些路由通过浏览器永远不会自动附加的 `Authorization`/`x-api-key` 头进行认证(无凭证会话/CSRF 暴露),并且 `Access-Control-Allow-Credentials` 永远不与回显/通配符配对。Cookie 认证的 **MANAGEMENT/dashboard 路由保持严格失败关闭**;`CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` 仍然优先。回归测试:`tests/unit/cors/origins.test.ts`、`tests/unit/authz/pipeline.test.ts`。(Bug 2 [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — 感谢 @jonlwheat2-gif) - **grok-web:** 转发 Cloudflare 验证 Cookie,停止将 IP 信誉拦截误标为错误 Cookie。"检测 Cookie"即使使用有效的完整浏览器会话也返回 `Invalid SSO cookie`——但 Cookie 解析器从来没有问题(它能从完整的 DevTools 头中稳健提取 `sso`/`sso-rw`)。修复了两个实际问题:**(1)** `buildGrokCookieHeader` 现在粘贴时会转发 `cf_clearance` 和 `__cf_bm`(之前会丢弃它们;AIClient2API 也会转发)——严格增量,裸 `sso` blob 仍然产生完全相同的 `sso=…`;**(2)** 当用户提供了 `cf_clearance` 时,来自 grok.com 的 401 / invalid-credentials-403 现在会显示为 IP 信誉/反机器人拦截(cf_clearance 与 IP+TLS+UA 绑定,无法从不同机器重放),而不是误导性的"无效 SSO Cookie——请重新粘贴"。没有 clearance 的裸 Cookie 仍然获得重新粘贴提示。回归测试:`web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`。([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — 感谢 @SeaXen) - **cli (serve):** `omniroute serve` 可选的原生 **HTTPS/TLS**——使严格 CSP 的 Electron 应用和浏览器可以通过 `https://` 访问 OmniRoute,而非纯 `http://localhost`。提供 `--tls-cert --tls-key `(或 `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`),独立服务器在同一监听端口上终止 TLS(无需额外端口/代理);WebSocket 升级(实时仪表盘 + `/v1` 流式传输)通过 `wss://` 正常工作,因为 `https.Server extends http.Server`。无 TLS 标志时 HTTP 路径与之前逐字节相同;仅提供 cert 或 key 其中之一、或路径不可读时,记录警告并保持 HTTP(永不会半启用,永不会崩溃)。本地主机的自动生成自签名证书作为后续任务;目前请提供显式的 cert/key(或在 OmniRoute 前部署 TLS 终结器)。回归测试:`tests/unit/tls-options.test.ts`。(Bug 1C [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — 感谢 @jonlwheat2-gif) - **opencode/observability:** 使 OpenCode Free 账户/代理轮换可见,并修复两个伴随发现的实际缺陷。**(1)** 每请求轮换选择日志(`dispatch via account … through proxy …`)级别为 `debug`(在默认 `APP_LOG_LEVEL=info` 下被隐藏)——提升为 `info`,使洗牌/冷却生命周期可审核(Token 保持脱敏)。**(2)** `[ProxyEgress]` 即使应用了账户代理也报告 `proxy=direct`,因为出口日志器位于执行器的嵌套代理上下文之外运行——现在有效应用的代理通过线程化到 proxy AsyncLocalStorage 的应用代理接收器来捕获,并反映在出口日志中。**(3)** `[callLogs] too many SQL variables`——`deleteCallLogRowsByIds` 在单个 `IN (…)` 中删除多达 5000 个 ID,超过 SQLite 的 ~999 绑定参数上限,导致日志裁剪/保留操作中断;ID 现在分块处理(每语句 ≤500)。回归测试:`tests/unit/call-log-trim-sql-vars-5217.test.ts`、`apply-executor-proxy-info-5217.test.ts`、扩展的 `opencode-proxy-rotation-4954.test.ts`。([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — 感谢 @daniij) - **chatgpt-web:** 将工具/函数调用接入 `chatgpt-web` 服务商。它是唯一从未读取 `body.tools` 的 Web 会话执行器——两个响应构建器都硬编码 `finish_reason:"stop"` 并仅输出内容,因此工具调用被静默丢弃(模型用散文回答)。现在使用共享的 `webTools` 提示模拟适配层(`` 约定的 system 消息 + `{…}` 响应解析),完全与其他 9 个同级执行器一致(qwen-web、perplexity-web 等)——它只是被 #3259 发布所遗漏。工具模式缓冲并输出 `tool_calls` + `finish_reason:"tool_calls"`(与图像生成路径隔离);纯聊天不变。回归测试:`tests/unit/chatgpt-web-tools-5240.test.ts`。([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — 感谢 @Rougler) - **oauth/dashboard:** 修复持久化/错误的 Antigravity "Token 已过期"徽章(#3679/#3850 的延续)。两个原因:**(1)** 新的 OAuth 连接从未设置 `tokenExpiresAt`(仅 `expiresAt`),因此仪表盘徽章——优先使用 `tokenExpiresAt || expiresAt`——回退到原始授权时钟,可能在首次后台刷新前闪烁错误的"Token 已过期"。现在创建时在所有 5 个 OAuth 创建路径中都将 `expiresAt` 镜像到 `tokenExpiresAt`(共享的 `buildOAuthConnectionCreatePayload`),与每个已经写入两者的刷新路径保持一致。**(2)** 当可刷新的连接没有可用的刷新 Token 时,健康检查扫描会静默跳过它,导致 `testStatus="active"` 永远保持而外观徽章却显示过期;现在会输出终止性的 `testStatus="expired"`("需要重新认证"),严格设置门控,永不会覆盖不可刷新的服务商或已终止/冷却状态。回归测试:`tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`、`tests/unit/token-health-no-refresh-token-expired-5326.test.ts`。([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) - **routing:** 对 API 密钥轮询连接(一个连接中有多个密钥的 `extraApiKeys`)在遇到上游 `402 "Insufficient account balance"` 时自动禁用已耗尽的 API Key。每个连接的路径已经将 402 终止化(→ `credits_exhausted`),但每个 KEY 的健康跟踪器(`recordKeyHealthStatus`)仅记录 `401` 的失败,因此 402 耗尽的密钥仍留在轮换中并不断重试。现在 402 通过新的 `recordKeyTerminal` 将当前密钥立即标记为无效(终止态——余额在会话中间不会恢复),因此轮换器跳过它并切换到下一个健康密钥;状态跨重启持久化。同时还向余额耗尽体信号中添加了 `insufficient balance`/`insufficient_balance`/`insufficient account balance`,使非 402 的余额不足响应也能终止化。回归测试:`tests/unit/key-health-402-disable-5239.test.ts`。([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — 感谢 @muflifadla38) - **cli:** `omniroute serve` 不再丢弃用户设置的 `NODE_OPTIONS=--max-old-space-size=…`。之前无条件地覆盖 `NODE_OPTIONS`(并传递显式的 `--max-old-space-size` CLI 参数)使用校准默认值,导致导出 `--max-old-space-size=8192` 的用户仍然以旧的限制运行并发生 OOM(#5238 报告者设置 8192,在约 505MB 时崩溃)。现在与 Electron 和独立启动器行为一致:如果 `NODE_OPTIONS` 已经固定了堆内存,该值优先(并抑制重复的 CLI 参数);否则追加校准的 `--max-old-space-size`,保留不相关的标志。回归测试:`tests/unit/serve-node-options-preserve-5238.test.ts`。(缺陷 C [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238);`b.mask`/OOM-root 部分单独跟踪。) - **dashboard:** 恢复服务商的**可用模型**工具栏中的 `{active}/{total} active` 模型数量徽章(服务商详情页)。它在 v3.8.13 的神文件分解(#3327)中被删除——`ModelVisibilityToolbar` 仍然接收 `activeCount`/`totalCount`,但它们被孤立为未使用的 `_` 前缀参数,渲染 `` 从未被迁移过来(`modelsActiveCount` i18n 键仍然保留)。重新连接现有 props 到现有键;零数据层或 i18n 更改。回归测试:`modelVisibilityToolbarActiveCount.test.tsx`。([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) - **rerank:** `/v1/rerank` 不再以 `400 "Invalid rerank model"` 拒绝 SiliconFlow 和 DeepInfra 的 Qwen3-Reranker 模型,即使 `/v1/models` 已列出它们。模型 ID 解析器从来不是问题(它已经在第一个斜杠处分割,因此 `siliconflow/Qwen/Qwen3-Reranker-8B` 解析正确)——`siliconflow` 和 `deepinfra` 只是缺失了 rerank 服务商注册。已添加两者:SiliconFlow 作为 Cohere 兼容,DeepInfra 通过新的 `deepinfra` 适配器(模型在 URL 路径中 `POST /v1/inference/`,`{queries,documents}` 请求,位置 `{scores}` 响应映射为 Cohere `results[]`)。回归测试:`tests/unit/rerank-providers-5332.test.ts`。([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — 感谢 @maikokan) - **authz/dashboard:** 当通过局域网 IP/非 localhost 主机访问仪表盘时,不再以 `403 INVALID_ORIGIN` 拒绝所有仪表盘变更操作。源绑定检查(#5278)只接受配置的 `*_PUBLIC_BASE_URL`(通常为 `http://localhost:20128`)加上内部的 `request.url` 源——而 Next.js standalone 报告的是绑定主机而非真实的 `Host`。因此在 `http://192.168.0.15:20128` 打开仪表盘会使浏览器的同源 `Origin` 无法匹配任何候选项,导致**所有** POST/PUT/DELETE(保存 API Key、保存服务商、测试连接)都失败,而 GET 仍能正常工作。两个修复:**(a)** 请求 `Host`(或受信任的 `X-Forwarded-Host`)现在被接受为有效的变更操作源,由两个独立检查控制——Token 标记的 socket 对端必须是 loopback/私有局域网**且** Host 本身也必须是 loopback/私有局域网 IP 字面量,因此 DNS 重绑定域名(分类为 `remote`)永远不能成为受信任的源,协议绑定到实际连接;(b) `INVALID_ORIGIN` 响应现在携带可操作的消息(设置 `OMNIROUTE_PUBLIC_BASE_URL`),仪表盘通过共享的 `extractApiErrorMessage` 辅助函数显示 API 错误 `.message`,而不是渲染原始错误对象。回归测试:`tests/unit/authz/public-origin.test.ts`(直接 LAN/loopback + DNS 重绑定防御)、`tests/unit/api-error-message-5340.test.ts`。([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) ### 📝 维护 - **chore(dead-code):** 全仓库清理未使用的导出符号,并设置对应的死代码基线递增门禁 — 修剪了未使用的导出辅助函数、校验/设置/加密配置 Schema、工具/domain/静态常量/格式化辅助函数、运行时测试辅助函数、请求超时 fetch 包装器、event-bus、semantic-cache(维护 + 过期)、correlation-middleware、MCP-scope、service-registry、build-profile、api-key-format、authz-class、models.dev-context、embedding-cache、provider-limits-scheduler、search-validator、webhook-example、agent-skills-repo-URL 和 command-code-auth-cleanup 导出。纯死代码移除,由 `typecheck:core` 验证(无剩余引用点)——无行为变更。([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362) — 感谢 @JxnLexn / @diegosouzapw) --- ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ --- ## [3.8.39] — 2026-06-28 ### ✨ 新功能 - **feat(oauth): 远程 Antigravity 登录 —— 本地助手 + 粘贴凭证** — Antigravity(及其他 Google "原生/桌面" OAuth 服务商)使用 Google 的 `firstparty/nativeapp` 授权页面,只有当 loopback 重定向(`127.0.0.1:`)可从授权浏览器访问时才释放认证码。在远程 VPS 安装中,该 loopback 存在于服务器上,因此授权页面永远挂起,从不发出认证码——"粘贴回调 URL"的退路方案无法使用(这是 Google 端的限制,上游 9router 同样存在)。新增 `omniroute login antigravity` CLI 助手在用户**本地**机器上运行 OAuth(127.0.0.1 可用),交换认证码,并打印一行 `omniroute-cred-v1.…` 凭证 blob;仪表盘的 Antigravity 连接 → 第 2 步字段现在接受该 blob(以及回调 URL),并通过新的 `paste-credentials` 操作持久化该连接(服务端引导,服务商白名单化,blob 中内嵌的服务商必须与路由匹配)。SSH 本地转发隧道作为零工具替代方案已被文档化。参见 [`docs/guides/REMOTE-MODE.md`](docs/guides/REMOTE-MODE.md)。([#5203](https://github.com/diegosouzapw/OmniRoute/pull/5203)) - **feat(agent-bridge): 容器/无头环境下的优雅证书安装回退** — 当 MITM 根 CA 无法自动安装到系统信任存储时(Docker / 无头 / 无 sudo / 只读信任存储),Agent Bridge 不再在启动时以通用"Certificate install failed"硬失败。它现在以跳过模式启动,仪表盘展示平台特定的**手动安装指南**(加上 CA 下载链接),运维人员可以手动信任证书。([#4546](https://github.com/diegosouzapw/OmniRoute/issues/4546) — 感谢 @phuchptty) - **feat(compression): CCR 范围/grep/统计检索(ReDoS 安全、向后兼容)** — 为 `omniroute_ccr_retrieve` MCP 工具和 `/api/compression/retrieve` 端点扩展 `range`(字节/行切片)、`grep`(ReDoS 安全的字面量或有界模式匹配)和 `stats`(字节/行/词计数)参数,代理可以精确获取需要的切片或摘要。所有参数均可选——无参数时返回完整块,行为与现有行为逐字节相同;CCR 存储完全兼容。压缩路线图第六项。([#5187](https://github.com/diegosouzapw/OmniRoute/pull/5187)) - **feat(compression): TOON 最优 N 候选编码器 + 编码器 A/B 对比表** — 将 `@toon-format/toon` 作为候选编码器通过最优 N 方案添加到 headroom 压缩引擎:每个提示同时运行 GCF 和 TOON,保留较短结果。压缩工作室中展示编码器 A/B 对比表(GCF/TOON/JSON——字节数和 cl100k Token)。压缩特性提取路线图第五项。([#5163](https://github.com/diegosouzapw/OmniRoute/pull/5163)) ### 🔧 问题修复 - **fix(oauth): Antigravity 刷新不再因上游空响应将存储的 refresh_token 置空** — Google 的 OAuth Token 端点使用非轮换刷新 Token:刷新响应通常不包含 `refresh_token`,偶尔会返回空字符串。Antigravity 执行器的 `refreshCredentials` 使用了 `typeof tokens.refresh_token === "string" ? tokens.refresh_token : credentials.refreshToken`,而 `typeof "" === "string"` 为真,因此空字符串响应将有效 Token 覆盖为空——首次刷新即将其置空。现在将非字符串**或空值**视为缺失,保留存储的 Token,与规范的 `refreshGoogleToken`(`tokens.refresh_token || refreshToken`)语义一致。([#3850](https://github.com/diegosouzapw/OmniRoute/issues/3850) — 感谢 @3xa228148) - **fix(api): LAN/Tailscale 仪表盘访问——`ws:` CSP 方案、版本路由 GET 豁免、展示 combo 字段错误** — 从非 loopback 主机打开仪表盘时的三个故障:(1) CSP `connect-src` 仅对 loopback 源允许 `ws:` 方案,阻止了 LAN/Tailscale 客户端的实时 WebSocket 连接;现在允许裸 `ws:`(与已允许的 `wss:` 对称);(2) `GET /api/system/version` 被 `LOCAL_ONLY_API_PREFIXES` 阻止——新增豁免白名单;(3) `COMBO_002` 校验错误现在在响应中包含首个 Zod 问题的字段和消息。([#5083](https://github.com/diegosouzapw/OmniRoute/issues/5083) — 感谢 @KooshaPari) - **fix(sse): 延迟 `` 关闭以防止其在 Claude→OpenAI 流式传输中泄漏到 `tool_calls` 之前** — 思考块后跟 tool_use 块时翻译器会注入伪造的助手文本区块。关闭标记现在推迟到第一个 `text_delta` 或流结束时刷新。([#5123](https://github.com/diegosouzapw/OmniRoute/issues/5123)) - **fix(sse): 规范化 Command Code 执行器中数组类型的用户消息内容以防止上游 400** — 用户消息 content 为数组时被逐字转发(Command Code 要求字符串),现在调用 `normalizeContentText()` 合并为字符串。([#5166](https://github.com/diegosouzapw/OmniRoute/issues/5166)) - **fix(mcp): 对未知/过期�� Streamable HTTP 会话 ID 返回 HTTP 404(而非 400)** — MCP 规范要求 404 以便客户端重新初始化。缺少会话 ID 的非初始化请求仍正确返回 400。([#5169](https://github.com/diegosouzapw/OmniRoute/issues/5169) — 感谢 @czer323) - **fix(api): 安全设置屏蔽"自动(零配置)"现在从 `/v1/models` 移除 `auto/*`** — 内置 `auto/*` 公告商忽略了 `settings.blockedProviders`。注入循环现在在 `auto` 被屏蔽时跳过整个 `auto/*` 块。([#5192](https://github.com/diegosouzapw/OmniRoute/issues/5192) — 感谢 @WslzGmzs) - **fix(cli): 根据物理 RAM 而非固定 512MB 默认值自动校准服务器 V8 堆内存** — 新 `calibrateHeapFallbackMb` 按物理 RAM ~35% 推导默认堆大小,限制 `[512, 4096]`。`OMNIROUTE_MEMORY_MB` 仍优先。([#5172](https://github.com/diegosouzapw/OmniRoute/issues/5172) — 感谢 @manchairwang, @Xyzjesus) - **fix(oauth): Antigravity 登录不再卡死——fire-and-forget 引导 + 有界 post-exchange** — `postExchange` 内联等待 `onboardUser` 重试循环导致登录无限旋转。现在 `onboardUser` 后台 fire-and-forget,`/exchange` 受 10 秒硬超时限制。([#5193](https://github.com/diegosouzapw/OmniRoute/pull/5193)) - **fix(antigravity): 在升级 combo 之前按配额族重试 Antigravity 账户** — 一个账户返回 429 时,combo 编排可能过早升级。现在先在同一配额族的其他账户重试。([#5180](https://github.com/diegosouzapw/OmniRoute/pull/5180) — 感谢 @Ardem2025) - **fix(translator): 在非流式 malformed-200 防护中接受 Claude Messages 格式** — 当 Claude 客户端路由到非 Claude 服务商时,翻译后的响应体为 Claude Messages 格式,防护层未识别该格式而返回 502。现在可识别 text/tool_use/thinking 块。([#5156](https://github.com/diegosouzapw/OmniRoute/pull/5156) — 感谢 @NomenAK) - **fix(sse): 通过参数 schema 匹配解析无名称的 deepseek-web `` 块** — 当 `chat.deepseek.com` 发出无名称 `` 块时,基于 schema 的退路方案将提取的参数名与各工具 schema 键进行比较以匹配名称。([#5154](https://github.com/diegosouzapw/OmniRoute/issues/5154)) - **fix(stream): 将服务商安全 finish_reason 规范化为 `content_filter`** — Gemini/Antigravity 可返回 SAFETY/RECITATION/BLOCKLIST 等,不再被下游识别。现在统一映射为标准 `content_filter`。([#5197](https://github.com/diegosouzapw/OmniRoute/pull/5197) — 感谢 @rdself) - **fix(responses): 在路由前规范化非数组 Responses API `input`** — OpenAI Responses API 接受 `input` 为字符串、对象或列表,但 OmniRoute 仅处理列表形式的载荷;字符串或对象 `input` 在 Responses→Chat Completions 路径上被静默丢弃。翻译器现在在调度前将 `input` 规范化为列表;Codex 原生 Responses 路径在转发前也进行规范化(防止上游 `400 Input must be a list`);并且提示注入和 PII 脱敏提取路径对对象值 `input` 进行了防护,使安全检查不会抛出异常。([#5204](https://github.com/diegosouzapw/OmniRoute/pull/5204) — 感谢 @wilsonicdev) - **fix(zenmux): 规范化 Z.AI 模型的服务商前缀 GLM 系统角色** — ZenMux 通过服务商前缀的 OpenAI 兼容 ID(如 `z-ai/glm-5.2`)暴露 Z.AI GLM。现有的 GLM 检测仅匹配裸 `glm-*`/`glm` ID,因此 `zenmux/z-ai/glm-5.2` 保持了系统消息不变;而 Z.AI 会拒绝以系统轮次结束的压缩历史(在 `assistant(tool_calls) → tool` 序列之前)。修复扩展了 GLM 检测以覆盖 `z-ai/glm-*` 前缀,并将其路由到现有的 `normalizeSystemRole` 路径。([#5158](https://github.com/diegosouzapw/OmniRoute/pull/5158) — 感谢 @Thinkscape) - **fix(xai): 添加 OAuth 连接测试探针 + 规范化 xAI 推理强度别名** — xAI 在服务商更新后对不支持的推理强度值(`max`、`xhigh`)返回 HTTP 400;xAI 翻译器现在在转发前将 `max` 和 `xhigh` 映射为 `high`。此外,xAI OAuth 连接缺少仪表盘测试配置,导致服务商测试返回 `"Provider test not supported"`;现在为 xAI 账户配置了专用的 OAuth 测试探针,并对强度规范化进行了回归覆盖。([#5157](https://github.com/diegosouzapw/OmniRoute/pull/5157) — 感谢 @nguyenxvotanminh3) - **fix(serve): honour `HOSTNAME` from `.env` instead of hardcoding `0.0.0.0`** — `bin/cli/commands/serve.mjs` spread `process.env` into the child-process environment but immediately overwrote `HOSTNAME` with a literal `"0.0.0.0"`, silently discarding any user-configured bind address even though `HOSTNAME` is documented in `.env.example` and `docs/reference/ENVIRONMENT.md`. `dist/server.js` already read `process.env.HOSTNAME` correctly; only the CLI wrapper was overriding it. The fix applies `process.env.HOSTNAME || "0.0.0.0"` so the env value takes effect. ([#5134](https://github.com/diegosouzapw/OmniRoute/issues/5134), [#5170](https://github.com/diegosouzapw/OmniRoute/pull/5170) — 感谢 @anki1kr / @Angelo90810) - **fix(cli): 强制 `NODE_ENV` 匹配自定义 Next 服务器中的 dev/start 运行模式** — 当 `.env.example` 设置 `NODE_ENV=production` 时,通过 `scripts/dev/run-next.mjs` 启动 `npm run dev` 会将该值转发给编程式 `next()` 入口,而该入口——为 `next` CLI 不同——不会将其规范化为匹配运行模式。由此产生的 production 标志导致 PostCSS 跳过 Tailwind 的 CSS 转换,在 `globals.css` 上表现为 `Module parse failed: Unexpected character @ sign`。自定义服务器现在显式强制 `dev` 路径使用 `NODE_ENV=development`,`start` 路径使用 `NODE_ENV=production`,忽略 `.env`。([#5189](https://github.com/diegosouzapw/OmniRoute/pull/5189) — 感谢 @backryun) - **fix(cli): 将开发服务器 Node 堆内存限制提升至 8 GB 以防止 OOM** — `npm run dev` 在编译重型仪表盘路由时因 `node scripts/dev/run-next.mjs` 使用 V8 默认的约 4 GB 堆内存(无 `--max-old-space-size` 标志)而崩溃,报错 `FATAL ERROR: Ineffective mark-compacts near heap limit — Allocation failed - JavaScript heap out of memory`。`dev` npm 脚本现在在调用时传递 `--max-old-space-size=8192`(这是为该进程设置此标志的唯一时机)。([#5198](https://github.com/diegosouzapw/OmniRoute/pull/5198) — 感谢 @backryun) - **fix(cli): re-enable Turbopack as the default `npm run dev` bundler** — PR #4092 forced webpack because an earlier Turbopack 16.2.x panic (`internal error: entered unreachable code: there must be a path to a root` in `turbopack-core/module_graph`) blocked the OmniRoute module graph. That panic no longer reproduces on the pinned Next 16.2.9, so `OMNIROUTE_USE_TURBOPACK` is flipped from `0` to `1` in `.env.example`, aligning it with `docs/reference/ENVIRONMENT.md` which had already documented the default as `1`. ([#5206](https://github.com/diegosouzapw/OmniRoute/pull/5206) — 感谢 @backryun) - **fix(auth): 允许 mimocode 使用合成无认证回退** — mimocode 连接在无显式凭证时会在到达执行器之前被阻止。认证层现在允许 mimocode 服务商的合成无认证回退,使无凭证访问模式按预期工作。([#5205](https://github.com/diegosouzapw/OmniRoute/pull/5205) — 感谢 @KooshaPari) - **fix(combo): 将空 Responses API `output: []` 视为容灾触发而拒绝** — 非流式 Responses API 体带有 `object: "response"` 和 `output: []` 时,被 Combo 响应质量校验器接受为有效 HTTP 200,导致 Combo 目标停止而非容灾到下一个分支。非流式校验器现在在通用 `output` 快捷方式之前检查 Responses-API 形状的响应体,并将空 `output: []` 拒绝为 `empty_choices`;结构性非空输出(如 `function_call`)仍然有效。([#5207](https://github.com/diegosouzapw/OmniRoute/pull/5207) — 感谢 @KooshaPari) - **fix(proxy): 在清除代理缓存时关闭缓存的调度器** — 缓存的代理和直连重试调度器在清除缓存时未被关闭,导致连接句柄泄漏。缓存清除路径现在对所有被逐出的调度器调用 `close()`;调度器缓存和生命周期辅助函数已从过度膨胀的 proxy-dispatcher 模块中提取到专用辅助模块以供复用。([#5202](https://github.com/diegosouzapw/OmniRoute/pull/5202) — 感谢 @KooshaPari) - **fix(proxy): 合并每个代理 URL 的并发快速失败健康探测** — 在高并发下,每个同时到达的请求都会为同一代理 URL 打开自己的 TCP 健康探测,造成惊群效应。并发代理快速失败检查现在被合并,每个代理 URL 同时只运行一个 TCP 探测;已完成的健康结果缓存被保留,使后续相同 URL 的检查立即返回。([#5109](https://github.com/diegosouzapw/OmniRoute/issues/5109), [#5208](https://github.com/diegosouzapw/OmniRoute/pull/5208) — 感谢 @KooshaPari) - **fix(pwa): 在显示离线页面之前优先使用缓存的导航** — Service Worker 在临时导航失败时过于急切地显示 `/offline`。现在它会缓存成功的导航响应,并在回退到 `/offline` 之前查询缓存的路由或应用壳;`/offline` 仅在无缓存导航或应用壳时作为最终回退保留。([#5165](https://github.com/diegosouzapw/OmniRoute/issues/5165), [#5209](https://github.com/diegosouzapw/OmniRoute/pull/5209) — 感谢 @KooshaPari) - **fix(request-logger): 永远不在压缩徽章中渲染负百分比** — 当每个提示 Token 都被压缩时(`totalIn = 0, compressed > 0`),压缩药丸徽章显示 `(-100%)`,因为徽章格式在百分比值前硬编码了前导 `-`。徽章现在在这种情况下省略负号,正确地将节省量表示为正向比例。([#5201](https://github.com/diegosouzapw/OmniRoute/pull/5201) — 感谢 @KooshaPari) - **fix(dashboard): 首页更新步骤警告图标使用 amber 色** — 首页更新步骤中的警告状态图标(`HomePageClient.tsx`)使用 `text-yellow-500`(Tailwind `#eab308`),在浅色背景上对比度很差(~1.9:1,低于 WCAG AA),且与同组件中所有同级元素使用的 `amber` 警告惯例不一致。切换为 `text-amber-500`——单行 `className` 变更,无行为变化。([#5176](https://github.com/diegosouzapw/OmniRoute/pull/5176)) ### 📝 维护 - **test(combo): 确定性 context-relay 通用交接覆盖** — 覆盖 `context-relay` 中通用的(与服务商无关的)会话交接路径(`combo.ts:2099–2139`),该路径此前仅有定义顺序断言和一个 `TODO(phase-2)`。测试通过会话接缝(`x-session-id` → `relayOptions.sessionId` → `maybeGenerateUniversalHandoff`)驱动真实管线,无需实时基础设施。([#5168](https://github.com/diegosouzapw/OmniRoute/pull/5168)) - **test(combo): 端到端 quota-share DRR 路由决策覆盖(矩阵对齐)** — 为 `quota-share` 策略添加缺失的 E2E 测试,通过进程内接缝驱动真实的 `handleChat` → chatCore → `selectQuotaShareTarget` → executor 管线,并断言分发的连接。DRR 选择器已有 29 个单元测试;此次补全了 E2E 缺口,使 quota-share 与 17 策略公开矩阵保持一致。([#5179](https://github.com/diegosouzapw/OmniRoute/pull/5179)) - **test(combo): 确定性 context-relay codex 配额交接覆盖(补全最后缺口)** — 覆盖 `context-relay` 中 codex 特定的交接块(`combo.ts:2143–2183`),该块在 #5168 中有记录但未经测试,因为需要 `codex` 连接。所有接缝(`fetchCodexQuota`、交接生成、会话中继)均以确定性方式 mock,无需实时基础设施。([#5195](https://github.com/diegosouzapw/OmniRoute/pull/5195)) - **test(ci): 将 antigravity-quota-family 测试纳入 `test:vitest`(修复测试发现孤儿)** — `open-sse/services/__tests__/antigravity-quota-family.test.ts`(由 #5180 引入)未被任何活跃运行器收集,导致 `check:test-discovery` 报告新的孤儿并阻塞发布分支上的每个后续 PR。该文件现已添加到 `vitest.mcp.config.ts` 的 `include` 中,对应的孤儿允许列表条目已移除。([#5196](https://github.com/diegosouzapw/OmniRoute/pull/5196)) - **test(security): 回归守护 — PII 脱敏保持可选(默认关闭)+ 第 20 号硬规则** — 添加测试断言 `PII_REDACTION_ENABLED` 和 `PII_RESPONSE_SANITIZATION` 特性标志的 `defaultValue` 字段均为 `"false"`,且两个标志均关闭时数据在三个应用点(`piiMasker`、`piiSanitizer`、`streamingPiiTransform`)中均未修改地通过,将第 20 号硬规则编码为 CI 强制执行的契约,并修复了 PII 脱敏默认开启的误导性文档表述。([#5159](https://github.com/diegosouzapw/OmniRoute/pull/5159)) - **docs(i18n): 添加繁体中文(zh-TW)README + 更新简体中文** — 新增繁体中文翻译(`docs/i18n/zh-TW/README.md`),并将简体中文 README 更新到当前英文基线;语言索引(`docs/i18n/README.md`)和根 `README.md` 徽章行相应更新。([#5162](https://github.com/diegosouzapw/OmniRoute/pull/5162) — 感谢 @lunkerchen) - **docs(i18n): zh-TW 和 zh-CN README 与标准英文 v3.8.39 全面同步** — 使两个翻译版本达到完全对齐,添加了完整的新功能章节、压缩真实 Token 示例以及 v3.8.38/39 英文 README 中更新的所有章节。([#5171](https://github.com/diegosouzapw/OmniRoute/pull/5171) — 感谢 @lunkerchen) - **docs(combo): 将 Combo/路由策略文档同步到当前状态 + 记录测试覆盖率** — 从 `README.md` 的 Fusion 条目中删除过时的序号;在 `docs/routing/AUTO-COMBO.md` 中新增**测试与覆盖率**章节,记录确定性策略矩阵(`npm run test:combo:matrix`)、quota-share DRR E2E 覆盖率和 v3.8.39 周期中交付的 context-relay 交接测试。([#5185](https://github.com/diegosouzapw/OmniRoute/pull/5185)) - **fix(docker):** 在 `npm ci` 之前复制 `open-sse` 工作区清单以确保仅工作区的依赖能够安装 — Dockerfile 仅复制了根目录的 `package*.json`,因此 `npm ci` 跳过了 `safe-regex` 和 `@toon-format/toon`(声明在 `open-sse/package.json` 中,未提升到根目录),导致多架构镜像构建在 `npm run build` 期间因 `Module not found` 而失败。感谢 @diegosouzapw --- ## [3.8.38] — 2026-06-27 ### ✨ 新功能 - **feat(sidebar): 彩色菜单图标** — 侧边栏菜单图标现在以每项主题色渲染:已知项使用精选颜色(`SIDEBAR_ICON_ACCENTS`),再加上基于哈希的确定性回退方案(`getSidebarIconAccent`),使每项在会话间获得稳定、独特的颜色。([#3812](https://github.com/diegosouzapw/OmniRoute/pull/3812) — 感谢 @rafacpti23) - **feat(providers): 新增 Factory (factory.ai) 订阅网关服务商** — `factory`(Factory Droids 的托管网关)现已成为 OpenAI 兼容端点 `https://api.factory.ai/v1` 上的一级路由服务商,使用 Bearer apikey 认证;密钥从仪表盘连接中提供(而非环境变量)。([#5065](https://github.com/diegosouzapw/OmniRoute/pull/5065) — 感谢 @KooshaPari) - **feat(providers): add Grok Build (xAI) provider with OAuth import-token flow** — `grok-cli` (alias `gc`) routes through Grok's CLI chat proxy; users paste their `~/.grok/auth.json` (or the JWT), with automatic `refresh_token` rotation. The public xAI client_id is embedded via `resolvePublicCred("grok_id")` (Hard Rule #11), never a literal. ([#5020](https://github.com/diegosouzapw/OmniRoute/pull/5020) — 感谢 @fulorgnas) - **feat(dashboard): 服务商页面中的模型别名支持点击编辑** — 点击别名即可内联编辑(Enter/失焦保存,Escape 取消),而不只能删除再重新添加。([#5119](https://github.com/diegosouzapw/OmniRoute/pull/5119) — 感谢 @waguriagentic) - **feat(providers): 新增 ZenMux Free(会话 Cookie 免费层)服务商** — `zenmux-free`(别名 `zmf`),配备专用执行器,将 ZenMux 的 Anthropic 风格 SSE 翻译为 OpenAI 格式;包含 12 个免费层模型(DeepSeek V3.2、GLM 4.7 Flash Free 等)。([#5105](https://github.com/diegosouzapw/OmniRoute/pull/5105) — 感谢 @mrnasil) - **feat(providers): 默认允许本地/私有服务商 URL(`Allow Local Provider URLs` 标志)** — 在 loopback/LAN 地址(如 `http://127.0.0.1:3264/api`)上添加/校验 OpenAI 兼容服务商时,会被 SSRF 守卫以 "Blocked private or local provider URL" 拒绝,尽管 OmniRoute 是本机优先的。新增 `OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS` 特性标志(默认**开启**,在设置 → 特性标志中切换),限定服务商校验守卫的范围:允许本地/私有主机,同时仍然阻止云元数据端点(169.254.169.254、metadata.google.internal)。禁用它可恢复到严格的仅公网模式。Webhook/远端镜像 SSRF 默认值不变。([#5066](https://github.com/diegosouzapw/OmniRoute/issues/5066),感谢 @daniij) - **feat(blackbox):** 刷新服务商模型目录,更新为最新模型。(感谢 @ptkelanatechsolutions) - **kiro**: 内联 `` 流分割器 — 当 `enabled` 存在时,`assistantResponseEvent` 内容现在被分割为独立的 `delta.content` / `delta.reasoning_content` SSE 块(新增 `open-sse/executors/kiroThinking.ts` 模块,接入 `KiroExecutor.transformEventStreamToSSE`)。 - **feat(cursor):** 解析 Cursor Composer DeepSeek 风格的内联工具调用 — Composer `cu/composer-2.5*` 模型使用 `<|tool▁calls▁begin|>…<|tool▁calls▁end|>` 标记而非结构化 protobuf 帧来嵌入工具调用;新的流式解析器(`composerToolCalls.ts`)在流式和非流式路径中拦截这些标记,从客户端可见内容中去除标记,并发出符合标准的 OpenAI `tool_calls` delta 供下游客户端原生处理。感谢 @noestelar - **feat(proxy):** 支持无认证的 `host:port` 批量导入,并展示代理测试失败信息。感谢 @dimaslanjaka - **feat(video): 阿里云 DashScope 视频服务商(`wan2.7-t2v`)** — 新增 `alibaba` 视频服务商(DashScope 异步任务 → 轮询 → MP4),通过标准 apikey 凭证路径接入,使文本转视频请求可路由到阿里的 `wan2.7-t2v` 模型。感谢 @josevictorferreira - **feat(cc): Claude-Code 兼容服务商的每连接"摘要思考显示"开关** — 暴露连接级别的开关,驱动现有的 Copilot 摘要思考标记,使运维人员可从 UI 将 CC 兼容连接配置为摘要推理显示(Schema + 请求默认值 + 服务商模态框,含 i18n)。感谢 @rdself - **feat(compression): 工作室中的压缩游乐场(Play + Compare 标签页)** — `/dashboard/compression/studio` 新增合成游乐场:粘贴文本 → 按引擎的**通道**(每个确定性引擎通过 `/api/compression/preview` 单独运行)以及按 `stackPriority` 排序的**组合瀑布流**,以及带有按需、**USD 上限**保真度评分的自由 **A/B Compare** 网格(`/api/compression/compare` + `compare/verify`)。预览路由现在使用真实的 cl100k tokenizer,返回 `engineBreakdown`,并接受有序的 `pipeline[]` 参数;新增 `compare` / `compare/verify` / `retrieve` 路由;实时 WS 订阅迁移至 `/dashboard/compression/live`。仅限管理权限。([#5080](https://github.com/diegosouzapw/OmniRoute/pull/5080)) - **feat(dashboard): 在 Combo 编辑器中暴露 Fusion `judgeModel` + `fusionTuning`** — Fusion 策略编辑器现在展示裁判模型(综合评审团答案;默认为第一个评审团模型)以及 quorum-grace 调优字段(`minPanel`、`stragglerGraceMs`、`panelHardTimeoutMs`),这些字段 `open-sse/services/fusion.ts` 已经读取。Schema 验证 + 有界;空调优永不持久化。([#5074](https://github.com/diegosouzapw/OmniRoute/pull/5074)) - **feat(compression): 级联管线的可选每步保真度门禁** — 每个压缩步骤现在可以由纯保真度检查器守护(4 个不变量,敞开后效),因此如果某个有损引擎会使提示降级超过阈值,则拒绝该步骤并跳过其通道,而不是静默输出。通过 `fidelityGate` 配置(高级阈值有意排除在 API 之外),并在工作室 Playground 开关中展示每个通道的拒绝明细。([#5143](https://github.com/diegosouzapw/OmniRoute/pull/5143)) - **feat(compression): 模糊近似去重(session-dedup 第二遍)** — session-dedup 引擎增加第二遍模糊匹配,折叠近似重复(而非仅字节完全相同)的片段,并提供工作室开关对比开启/关闭效果。([#5143](https://github.com/diegosouzapw/OmniRoute/pull/5143)) - **feat(quota): 可选的 Codex/Claude 自动 ping 保活** — 可选的后台保活功能可定期 ping Codex/Claude 连接以保持其会话/配额状态预热,减少首次真实请求的冷启动失败。([#5102](https://github.com/diegosouzapw/OmniRoute/pull/5102)) - **feat(ops): SRE 运维手册 + 运维辅助脚本** — 从已关闭的过期 PR 中抢救而来;新增运维 Runbook 和运维辅助脚本。([#5138](https://github.com/diegosouzapw/OmniRoute/pull/5138) — 感谢 @KooshaPari / @diegosouzapw) - **feat(mcp): Web 会话健壮性——Cookie 去重 + 浏览器池可观测性** — MCP Web 会话路径现在在(重新)加载会话时对 Cookie 进行去重(避免冲突的重复 `Cookie` 头),并为无头 Web 服务商暴露浏览器池可观测性(池大小 / 使用中 / 获取指标)。([#5121](https://github.com/diegosouzapw/OmniRoute/pull/5121),基于 [#3368](https://github.com/diegosouzapw/OmniRoute/issues/3368)) - **feat(compression): Ionizer 引擎——有损 JSON 数组采样,可通过 CCR 恢复** — 新的压缩引擎,将大型 JSON 数组下采样为代表性子集,并记录紧凑变更表示(CCR),以便可以重建被省略的行,在表格/数组密集型载荷上以精确性换取大幅 Token 减少。([#5148](https://github.com/diegosouzapw/OmniRoute/pull/5148)) ### 🔧 问题修复 - **fix(proxy): 使 SOCKS5 握手超时可运维调整(`SOCKS_HANDSHAKE_TIMEOUT_MS`)** — 在针对同一住宅网关主机的高并发下,SOCKS5 连接握手可能超过硬编码的 10 秒,即使代理可达,也会表现为虚假的 `[Proxy Fast-Fail] Proxy unreachable`(池大小已可通过 `OMNIROUTE_PROXY_DISPATCHER_CONNECTIONS` 调整)。握手超时现在读取 `SOCKS_HANDSHAKE_TIMEOUT_MS`(默认值保持 `10000`,上限 `120000`),使高并发部署可以无需代码更改提高此值。对 #5109 的缓解措施(完整的 concurrency-100 崩溃仍需报告者的现场压力测试确认)。([#5109](https://github.com/diegosouzapw/OmniRoute/issues/5109)) - **fix(api): 不区分大小写地解析 `GET /v1/models/{id}`** — 规范化模型 id 的客户端(如 OpenCode 请求 `minimax/minimax-m3` 查找标准目录条目 `minimax/MiniMax-M3`)会错过区分大小写的单模型查询,回退到显示 `context_length: 0`。`findModelById` 现在优先精确大小写匹配,回退到不区分大小写匹配,因此无论大小写如何都返回真实条目(及其上下文窗口)。([#5082](https://github.com/diegosouzapw/OmniRoute/issues/5082)) - **fix(services): 内嵌 WS 代理遵循 `LIVE_WS_HOST`;早期拒绝空 `messages`** — 两个无头/Docker 部署修复(#5110)。内嵌 WebSocket 代理(`:20131`)仅读取 `EMBED_WS_PROXY_HOST`,因此在反向代理/隧道后即使设置了 `LIVE_WS_HOST=0.0.0.0` 仍然绑定在 `127.0.0.1`,实时仪表盘显示"WebSocket disconnected";现在回退到 `LIVE_WS_HOST`(默认仍为 loopback)。此外,显式空 `messages: []` 数组的请求此前被转发到上游并以混乱的原始 `400/502` 返回;`handleChat` 现在以明确的 `messages: at least one message is required` 预先拒绝(Responses-API `input` 请求不受影响)。([#5110](https://github.com/diegosouzapw/OmniRoute/issues/5110)) - **fix(proxy): 修复一键 Deno 和 Cloudflare 中继部署** — `/api/settings/proxy/test` 端点仅识别 `vercel` 中继类型,因此测试已部署的 Deno 或 Cloudflare 中继会返回 `proxy.type must be http, https, or socks5` 而从未到达中继;现在通过 `isRelayType()` 路由所有中继类型。在有 `STORAGE_ENCRYPTION_KEY` 的安装中,中继认证 Token 通过 `extractRelayAuth`(加密的 `relayAuthEnc` 形式)读取,修复了导致 `publicIp` 为 null 的静默 `401`。Cloudflare Worker 上传现在将脚本部分作为 `application/javascript` 发送(API 拒绝 `application/javascript+module`;ES 模块语义来自 `main_module`),且代理注册表 Schema 接受 `deno`/`cloudflare` 类型和 `deno-relay`/`cloudflare-relay` 来源,使编辑已部署的中继不再返回 400。([#5128](https://github.com/diegosouzapw/OmniRoute/issues/5128)) - **fix(kiro): 从 Kiro 目录中移除 `claude-sonnet-4.5` + 精确匹配 Kiro 400 错误** — `claude-sonnet-4.5` 已离开 Kiro 免费层阵容(当前活跃模型:Opus 4.8/4.7/4.6、Sonnet 4.6、Haiku 4.5),因此从 Kiro 注册表条目和免费模型目录中移除。回归测试现在将 Kiro 的字面 `[400] Invalid model. Please select a different model to continue.` 精确匹配到 `isModelUnavailableError` 模型不可用分类。所有模型(包括当前模型)都返回 400 则指向服务端 Kiro 层级/区域门禁,而非 OmniRoute 目录错误。([#5140](https://github.com/diegosouzapw/OmniRoute/pull/5140),关闭 [#4484](https://github.com/diegosouzapw/OmniRoute/issues/4484)) - **fix(dashboard): preserve every rendered field when loading/saving Resilience settings** — `ResilienceTab` renders `comboCooldownWait` and `quotaShareConcurrencyLimit`, but both the initial-load and save paths rewrote component state without those fields, so after a successful `/api/resilience` response the cards received `undefined` and the page fell back to the generic "failed to load" state. A shared `toResilienceResponse()` mapper now keeps all rendered fields, and `PATCH /api/resilience` returns `quotaShareConcurrencyLimit` to match GET and the UI contract. ([#5139](https://github.com/diegosouzapw/OmniRoute/pull/5139) — 感谢 @rdself) - **fix(quota): 从快照中加载内存配额缓存 + 限定 Auto-Combo 候选范围** — 重启后配额缓存为空,因此已知已耗尽的连接在重新查询前看起来是健康的;`isAccountQuotaExhausted` 现在从持久化的 `quota_snapshots` 中延迟加载。Auto-Combo 候选扩展也限定为每个 Combo 目标实际允许的连接,而不是拉入该服务商的所有连接。([#5015](https://github.com/diegosouzapw/OmniRoute/pull/5015) — 感谢 @JxnLexn) - **fix(resilience): 加固配额截断、Gemini 音频 MIME 和模型锁定冷却** — 存储的配额硬截断值不再从任意字符串强制转换为 `enabled=true`;Gemini 音频输入部分在转发前对其 MIME 类型进行校验/规范化;模型锁定现在遵循配置的 `maxCooldownMs` 上限。([#5093](https://github.com/diegosouzapw/OmniRoute/pull/5093) — 感谢 @KooshaPari) - **fix(streaming): 加固长时间的 OpenAI 兼容 SSE 流** — 管线末尾阶段结束的错误不再覆盖已记录的流成功状态(`streamCompletionRecorded` 守卫),客户端断开连接最终化为 `499 client_disconnected` 而非污染服务商/账户失败状态,实际上是 SSE 的 JSON 体(错误的 `application/json` content-type)被嗅探并重新流式传输,推理字段(`reasoning`/`reasoning_content` + OpenRouter/Gemini 加密的 `reasoning_details`)通过 JSON-as-SSE 回退得以保留。([#5124](https://github.com/diegosouzapw/OmniRoute/pull/5124) — 感谢 @rdself) - **fix(usage): 对请求用量日志去重并防抖统计事件** — `saveRequestUsage` 现在防止重复插入(自然键:timestamp + provider + model + connection + api-key + token 计数),补全缺失的 `endpoint`,并仅在实际插入行时才发出 `usageRecorded`;统计 `update`/`pending` 事件突发被折叠为单次防抖通知以减少抖动。([#4940](https://github.com/diegosouzapw/OmniRoute/pull/4940) — 感谢 @nguyenxvotanminh3) - **fix(sse): 在 Antigravity MITM 处理器中将原生 Gemini 请求体转换为 OpenAI 格式** — `contents` / `systemInstruction` / `generationConfig` / `thinkingConfig` 现在在转发到 `/v1/chat/completions` 之前被翻译为 OpenAI chat-completions 格式,使支持思考的模型(如 `ag/claude-opus-4-6-thinking`)不再因服务商返回 400 "invalid argument" 错误而失败。([#4845](https://github.com/diegosouzapw/OmniRoute/pull/4845) — 感谢 @anuragg-saxenaa) - **fix(db): 将两条 pt-BR SQLite 驱动回退日志行翻译为英文** — `[DB] Pré-inicializando sql.js WASM…` 和 `[DB] Drivers síncronos indisponíveis…` 是唯一的非英文服务器日志字符串,导致日志中语言混用。现在改为 `[DB] Pre-initializing sql.js WASM (synchronous drivers unavailable)…` / `[DB] Synchronous drivers unavailable — falling back to sql.js (WASM)`,并通过扫描驱动路径中带重音符号的日志字符串的测试来守护。([#5103](https://github.com/diegosouzapw/OmniRoute/issues/5103)) - **fix(diagnostics): 非流式 Claude 响应不再因 `empty_choices` 而被误判为 502** — v3.8.37 的畸形 200 检测器(#4942)仅理解 OpenAI `choices` 和 Responses-API `output` 形状,因此保持 Claude 形状(`{type:"message", content:[…]}`)的 `/v1/messages` 响应落入 `empty_choices` → 502(在 Combo 中级联为 "All models failed")。最明显的是,扩展思考轮次中缓冲体为单个**携带有效 `signature` 的空思考块**(Claude Code 的非流式 Bash 分类器)在每次调用时都返回 502。`detectMalformedNonStream` 现在理解 Claude 形状:text/tool_use 块和携带签名的思考块计为有效输出,而真正空的 `content:[]` 仍被标记。([#5108](https://github.com/diegosouzapw/OmniRoute/issues/5108),感谢 @insoln) - **fix(combo): 空内容 502 现在在同一请求内容灾,而不是耗尽服务商** — 返回 HTTP 200 但无可用于完成正文的分支被重写为 `502 "Provider returned empty content"`,但 Combo 耗尽分类器将该合成 502 视为连接级故障(`#1731v2`)并将整个服务商/连接标记为已耗尽,跳过该请求中所有剩余的**同服务商**分支。连接实际上是健康的(只是返回了空内容),因此空内容 502 现在被分类为模型级瞬态故障:请求推进到下一个分支,该服务商其余分支仍保持可选。真正的网关 502 仍然触发连接耗尽。([#5085](https://github.com/diegosouzapw/OmniRoute/issues/5085),感谢 @andrea-kingautomation) - **fix(dashboard): 展示详细的凭证校验错误,而非仅仅显示"invalid"徽章** — 添加连接模态框中的内联"检查"丢弃了 `/api/providers/validate` 返回的 `error` 消息,只显示一个 `invalid` 徽章。对于 Web 服务商(claude-web / chatgpt-web),真正的原因往往是后端已报告的环境错误(如 `TLS impersonation client failed to start: EACCES … mkdir tls-client-node/bin`),因此用户只能猜测。模态框现在在徽章旁边渲染完整的原因文本。([#5088](https://github.com/diegosouzapw/OmniRoute/issues/5088),感谢 @tkhs101) - **fix(executors): 从转发给 Cerebras 和 Mistral 的请求体中剥离 `client_metadata`** — 当透传请求体包含 `client_metadata`(OpenAI Codex / Claude CLI 字段,这些上游无对应项)时,Cerebras 返回 400(`wrong_api_format`),Mistral 返回 422(`extra_forbidden`)。默认执行器现在在向下游发送前为这两个服务商移除该字段;其他服务商(特别是 `openai`/`codex`)保留它。(感谢 @saurabh321gupta) - **fix(codebuddy):** 仅在客户端请求推理时才发送推理参数。(感谢 @anki1kr) - **fix(sse):** 当 JSON 客户端请求时,为强制流式传输服务商保持流式传输。被标记为 `forceStream:true` 的服务商拒绝 `stream:false` 上游(HTTP 400);`resolveStreamFlag` 现在对此进行防护,使仅支持流式的服务商即使在客户端发送 `Accept: application/json` 或 `stream:false` 时也保持流式传输。(感谢 @anki1kr) - **fix(sse):** 防止非 JSON SSE 行和重复 `[DONE]` 破坏客户端。(感谢 @qianze0628) - **fix(sse):** 在执行器 `buildHeaders` 路径中对大小写变体的 Anthropic 头去重 — Node/undici 的 `fetch` 将 `anthropic-version` 和 `Anthropic-Version` 合并为单个 `"v, v"` 值,Anthropic API 会拒绝该值,因此两种大小写变体现在折叠为一个规范的小写头(`anthropic-beta` 同理)。(感谢 @Delcado19) - **oauth(kiro):** 支持 Kiro IDC(组织)Token 导入 — 当 `~/.aws/sso/cache` Token 携带 `clientIdHash` 时,自动导入现在读取链接的客户端注册文件以获取 `clientId`/`clientSecret`,探测 Kiro IDE `profile.json` 中的 `profileArn`(ARN 区域规范化为 `us-east-1` 用于运行时网关),并通过区域 AWS OIDC 端点而非社交路径进行刷新;导入 Schema 和模态框转发这些凭证,因此手动导入也适用于 IDC Token。感谢 @enjoyer-hub - **fix(translator):** 当将 Claude 格式请求(如 Claude Code)路由到阿里云 DashScope 的 OpenAI 兼容服务商(`alibaba` / `alibaba-cn`)时,保留客户端 `cache_control` 断点。Claude→OpenAI 翻译之前从系统和消息文本块中剥离了这些标记,因此 DashScope 的显式缓存从未启用,每次请求都是缓存未命中。现在当为支持缓存能力的 OpenAI 格式服务商请求保留时,缓存提示得以保留。(感谢 @sacrtap) - **fix(tts):** 从目录中解析 Gemini TTS 模型,并添加 `gemini-3.1-flash-tts-preview` 作为新的默认 Vertex TTS 模型。(感谢 @nguyenha935) - **fix(sse): 不要在自身上游超时(504)导致健康连接进入冷却** — 当 OmniRoute 自身的截止时间到达时(表现为 `TimeoutError`/`BodyTimeoutError` → 504),连接不再被禁用/容灾切换,因此缓慢但健康的服务商不会因我们的超时而受惩罚。真正的上游 5xx/429 仍触发冷却;Antigravity 保持自己的策略。感谢 @costaeder - **fix(translator):** 将图像 `tool_result` 块作为 `image_url` 转发,而不是将 base64 字符串化。(感谢 @alican532) - **fix(sse): 稳健的 Anthropic `/v1/messages` 流式传输——真实 ping 保活 + 客户端断开守卫** — 推理模型缓慢的首个 Token 可能触发严格客户端的空闲读看门狗;该路由现在从第一帧开始以真实的 `event: ping`(Anthropic 客户端忽略 SSE 注释)保持流连接活跃,且客户端断开(AbortError / controller-closed)不再被计为服务商故障(无容灾/冷却)。感谢 @costaeder - **fix: 在模型同步期间保留模型隐藏标志(`isHidden`)** — `replaceCustomModels` 将兼容覆盖列表修剪为新的自定义模型 ID,在每次定期同步/导入时静默擦除已隐藏的同步模型的 `isHidden` 标志(所有隐藏模型重新变为可见)。移除了冗余清理(每个模型的删除已经处理其自身的兼容清理),因此隐藏模型在重新同步期间保持隐藏。([#5086](https://github.com/diegosouzapw/OmniRoute/pull/5086) — 感谢 @herjarsa) - **fix(models): 从注册表 `modelsUrl` 推导模型发现配置** — 不在硬编码 `PROVIDER_MODELS_CONFIG` 中但携带注册表 `modelsUrl` 的服务商(如 MiniMax),现在自动获得 Bearer `/v1/models` 发现配置,因此"发现模型"能正常工作而不是返回空。感谢 @herjarsa - **fix(compression): 通过运行时锚点解析 Worker + 规则/过滤器资源文件(独立打包)** — LLMLingua Worker 和 RTK 规则/过滤器加载器依赖 `fileURLToPath(import.meta.url)`,而独立打包会将其冻结为构建机器路径,因此 Worker 永不启动、规则/过滤器包无法解析。现在基于 `process.cwd()`/`argv[1]` 进行锚定(Worker URL 使用 `pathToFileURL`)。感谢 @fulorgnas - **fix(api): 对 7 个管理路由的错误响应进行脱敏处理(第 12 号硬规则加固)** — `cli-tools/backups`、`cli-tools/guide-settings/[toolId]`、`logs/export`、`models/catalog`、`providers/test-batch`、`settings/import-json` 和 `usage/proxy-logs` 不再返回原始 `error.message`;它们将捕获的错误包装在 `sanitizeErrorMessage(...)` 中,且这些路由已从 `check-error-helper` 白名单中移除。(感谢 @JxnLexn) - **fix(sse): 防止仅 `output_text` 的 Responses 正文被丢弃/误判为 502** — 某些上游返回简写的 Responses 正文,其答案仅在 `output_text` 中,`output[]` 为空。`sanitizeResponsesApiResponse` 丢弃了文本,导致响应随后触发了畸形 200 守卫。脱敏器现在从非空 `output_text` 合成一个 `output[]` 消息项(补充 #5108 中的 Claude 原生修复;两者均源自 #4942)。 - **fix(executors): 保留调用者单一大写的 `Anthropic-Version` 头** — 大小写变体去重(#4846)无条件地将 `Anthropic-Version`/`Anthropic-Beta` 重写为小写,即使只有一个变体存在,破坏了调用者的头部。去重现在仅在两种大小写变体同时存在时运行(该功能原本要修复的 undici-merge 冲突场景)。 - **fix(responses):** 为 openai-compatible **responses** 服务商默认设置 `text.format` 为 `{ type: "text" }` — 某些 Responses 兼容上游(如 LM Studio)会以 400 `missing_required_parameter` 拒绝缺少 `text.format` 的 `text` 对象;默认执行器现在在转发前填充 Responses-API 默认值(限定于 `openai-compatible-*responses*`,永不覆盖已有格式)。感谢 @StevanusPangau - **fix(translator): 停止对推理重放服务商剥离客户端提供的 `reasoning_content`** — #4849 的 agentic-context 剥离(从工具调用助手轮次中删除 `reasoning_content` 以避免 O(n²) Token 增长)无条件运行,因此重放服务商(DeepSeek V4、Kimi K2、Qwen-Thinking 等)丢失了客户端的推理,推理重放缓存随后用过期的缓存值覆盖它(此类上游在缺少原始推理时会返回 400)。剥离现在跳过推理重放目标,非推理服务商保持 O(n²) 保护。([#5122](https://github.com/diegosouzapw/OmniRoute/pull/5122)) - **fix(providers): 将 MiniMax M3 和 Nemotron 3 Ultra 添加到 Cline 目录** — 这两个模型在 Cline 的服务商目录中缺失且无法选择;两者现在已注册。([#5136](https://github.com/diegosouzapw/OmniRoute/pull/5136),关闭 [#3321](https://github.com/diegosouzapw/OmniRoute/issues/3321)) - **fix(dashboard): 基于标准 `providerId` 的模型可见性开关** — 每个模型的可见性开关此前基于显示 ID,因此在一个服务商别名上切换模型可能会误操作到另一个;现在基于标准 `providerId`。([#5091](https://github.com/diegosouzapw/OmniRoute/pull/5091) — 感谢 @Theadd) - **fix(diagnostics): 在 `detectMalformedNonStream` 中识别 Claude API 格式** — 抢救性添加 null 守卫,使 Claude 形状的非流式正文不再被错误分类。([#5141](https://github.com/diegosouzapw/OmniRoute/pull/5141) — 感谢 @herjarsa / @diegosouzapw) - **fix(logging): 在容灾日志中跟踪最终的连接 ID** — 容灾日志行现在记录实际服务(或最后失败)该请求的连接,而不仅仅是首次尝试。([#5016](https://github.com/diegosouzapw/OmniRoute/pull/5016) — 感谢 @JxnLexn) - **fix(sse): 在带内流错误处理期间忽略断开竞争** — 与带内上游错误处理竞争的客户端断开连接,不再表现为虚假的服务商故障。([#5007](https://github.com/diegosouzapw/OmniRoute/pull/5007) — 感谢 @JxnLexn) - **fix(dashboard): 在 `handleToggleCombo` 失败时展示服务端错误** — Combo 切换失败现在显示后端错误,而不是静默无操作。([#5138](https://github.com/diegosouzapw/OmniRoute/pull/5138) — 感谢 @KooshaPari / @diegosouzapw) - **fix(quota): 跟踪服务商配额重置窗口 + 充实 Codex 工作室** — 观察到的配额重置窗口被跟踪并展示,Codex 工作室获得丰富的配额元数据。([#5141](https://github.com/diegosouzapw/OmniRoute/pull/5141) — 感谢 @Witroch4 / @diegosouzapw) - **fix(sidebar): 删除孤立的 `settings` 主题色** — 移除一个导致 `typecheck:core` 失败的悬空主题色条目。([#5142](https://github.com/diegosouzapw/OmniRoute/pull/5142)) - **fix(sse): 为兼容客户端保留非流式推理字段** — 非流式响应现在保留上游推理字段(`reasoning` / `reasoning_content` 和 OpenRouter/Gemini `reasoning_details`),而不是在 `responseSanitizer` 中剥离它们,使渲染缓冲响应推理的客户端不再丢失推理内容。([#5155](https://github.com/diegosouzapw/OmniRoute/pull/5155) — 感谢 @rdself) - **fix(i18n): 添加缺失的英文 UI 标签** — 补全在仪表盘中以原始键名显示的未翻译英文字符串。([#5153](https://github.com/diegosouzapw/OmniRoute/pull/5153) — 感谢 @rdself) ### 🔒 安全 - **fix(security): 精确主机名的 Anthropic `baseUrl` 检查** — Anthropic base-URL 守卫使用了子串匹配,精心构造的主机可以部分满足条件;现在要求精确主机匹配(解决 CodeQL `js/incomplete-url-substring-sanitization` 告警 #674)。([#5130](https://github.com/diegosouzapw/OmniRoute/pull/5130)) ### 📝 维护 - **refactor(store): 移除死代码中的旧版 store 模块** — 抢救性清理未使用的旧版 store 代码。([#5138](https://github.com/diegosouzapw/OmniRoute/pull/5138) — 感谢 @JxnLexn / @diegosouzapw) - **test(combo): 全部 17 策略的确定性路由决策矩阵** — 确定性 E2E 矩阵固定了每个 Combo 策略的路由决策。([#5146](https://github.com/diegosouzapw/OmniRoute/pull/5146)) - **chore:** baseline reconciliations (complexity / file-size / cognitive), golden-snapshot + apikey-count alignment for new providers, orphan-test relocation, release base-red repairs, CHANGELOG i18n mirror sync, and an `actions/cache` 5→6 bump. ([#5145](https://github.com/diegosouzapw/OmniRoute/pull/5145), [#5144](https://github.com/diegosouzapw/OmniRoute/pull/5144), [#5125](https://github.com/diegosouzapw/OmniRoute/pull/5125), [#5126](https://github.com/diegosouzapw/OmniRoute/pull/5126), [#5120](https://github.com/diegosouzapw/OmniRoute/pull/5120), [#5117](https://github.com/diegosouzapw/OmniRoute/pull/5117), [#5112](https://github.com/diegosouzapw/OmniRoute/pull/5112)) - **test:** 带门控的 Combo 策略实况冒烟测试(进程内 + VPS HTTP)并刷新发布期望以匹配当前代码。([#5151](https://github.com/diegosouzapw/OmniRoute/pull/5151), [#5150](https://github.com/diegosouzapw/OmniRoute/pull/5150) — 感谢 @KooshaPari / @diegosouzapw) --- ## [3.8.37] — 2026-06-26 ### ✨ 新功能 - **feat(providers):** 新增 DGrid AI 网关服务商 — OpenAI 兼容网关,位于 `api.dgrid.ai/v1`(别名 `dgrid`,API-key 认证,透传模型)。免费路由层(10 RPM / 100 RPD);$5 终身充值将限制提升至 20 RPM / 1,000 RPD。([#4931](https://github.com/diegosouzapw/OmniRoute/pull/4931) — 感谢 @dgridOP) - **feat(providers):** 新增 Pioneer AI(Fastino Labs)服务商 — OpenAI 兼容的聊天补全,位于 `api.pioneer.ai/v1`。注册别名 `pn`,`X-API-Key` 认证,包含 10 个开放层无服务器模型目录(Qwen3、Llama 3.1/3.2、Gemma 3、SmolLM3)。免费 $75 积分,无需信用卡。企业模型(Claude/GPT/Gemini)需要先在 Pioneer 平台上进行微调,有意排除在目录之外。([#4909](https://github.com/diegosouzapw/OmniRoute/pull/4909) — 感谢 @HikiNarou) - **feat(providers):** 新增 xAI Grok 入站翻译器和思考修补器 — Grok 请求现在在入站路径上被翻译,推理被规范化,使 Grok 模式在不同客户端间行为一致。([#4910](https://github.com/diegosouzapw/OmniRoute/pull/4910) — 感谢 @mugnimaestra) - **feat(oauth):** Codex 批量导入端点 — `POST /api/oauth/codex/import` 接受一次调用多个 Codex OAuth 凭证,实现快速多账户接入。([#4914](https://github.com/diegosouzapw/OmniRoute/pull/4914) — 感谢 @beaaan) - **feat(embeddings):** 为嵌入 Combo 添加 `dimensions` 覆盖字段,使嵌入 Combo 可以按目标固定输出向量大小。([#4913](https://github.com/diegosouzapw/OmniRoute/pull/4913) — 感谢 @wenzetan) - **feat(sse):** 成功 Combo 模型自动提升 — 新的可选 `comboAutoPromoteEnabled` 设置重新排序 Combo 持久化的模型列表,当 Combo 模型成功响应时,它会被移到未来请求的第 #1 位置。([#4852](https://github.com/diegosouzapw/OmniRoute/pull/4852) — 感谢 @arssnndr) - **feat(sse):** 添加可切换的工具来源诊断 — 可选开关在调试工具路由问题时显示每个工具定义的来源。([#4856](https://github.com/diegosouzapw/OmniRoute/pull/4856) — 感谢 @DuyPrX) - **feat(headroom):** 代理生命周期管理 + 仪表盘 UI — 从仪表盘启动/停止/监控 Headroom 压缩代理,支持 Docker sidecar。([#4649](https://github.com/diegosouzapw/OmniRoute/pull/4649) — 感谢 @diegosouzapw / @carmelogunsroses) - **feat(sse):** `x-omniroute-strip-reasoning` 请求头用于忽略上游响应中的 `reasoning_content`(可选,保留推理感知客户端)。([#4678](https://github.com/diegosouzapw/OmniRoute/pull/4678) — 感谢 @anuragg-saxenaa / @diegosouzapw) - **feat(cli):** Factory Droid CLI 集成的多模型支持。([#4682](https://github.com/diegosouzapw/OmniRoute/pull/4682) — 感谢 @anuragg-saxenaa / @diegosouzapw) - **feat(sse):** 从结构化 `RetryInfo` 载荷解析 Gemini CLI 429 `retryDelay`,使冷却遵循上游提供的退避时间。([#4738](https://github.com/diegosouzapw/OmniRoute/pull/4738) — 感谢 @NoxzRCW) - **feat(sse):** 将 GPT-4 和 GPT-4o mini 添加到 GitHub Copilot 服务商目录。([#4798](https://github.com/diegosouzapw/OmniRoute/pull/4798), [#4797](https://github.com/diegosouzapw/OmniRoute/pull/4797) — 感谢 @decolua) - **feat(api):** 添加 `MiniMax-M3` 定价行(标准名称 + 小写别名),使新的 MiniMax 默认模型获得准确的每次请求成本核算,而非回退到零/默认费率。([#4814](https://github.com/diegosouzapw/OmniRoute/pull/4814) — 感谢 @octo-patch) ### 🔧 问题修复 - **fix(sse):** `response.completed` 中稠密、确定性的 `response.output` 排序 — 项目现在按其实际 `output_index` 排序(通过记录已发出的累加器 + 稳定排序),而不是从无序状态字典重建;`normalizeOutputIndex` 替换了脆弱的 `parseInt` 调用以实现稳健的索引转换;流中同一索引被替换的工具调用被排除在最终输出数组之外。([#4906](https://github.com/diegosouzapw/OmniRoute/pull/4906) — 感谢 @Marco9113) - **fix(sse):** 将 Codex 自定义/自由格式工具(`apply_patch`、无 `parameters` 的 `type:"custom"`)规范化为 `{ input: string }` 函数 Schema 而非空 Schema — 空 Schema 使模型使用 `{}` 调用 `apply_patch`,破坏了期望 `{ input: string }` 的 Codex 运行时。还将 `custom_tool_call` / `custom_tool_call_output` 输入项映射,并通过 `custom_tool_call_input.delta`/`.done` 事件流式传输 `apply_patch` 工具调用。([#4862](https://github.com/diegosouzapw/OmniRoute/pull/4862) — 感谢 @nstung463) - **fix(sse):** 在翻译 Draft 2020-12 Antigravity 工具 Schema(如来自 OpenCode)时保留 `required` 数组,剥离不受支持的 JSON Schema 元关键词,同时保留必需参数使模型不再在调用工具时不带必需参数。([#4843](https://github.com/diegosouzapw/OmniRoute/pull/4843) — 感谢 @anuragg-saxenaa) - **fix(sse):** Kiro 工具 Schema 脱敏器 — 在调度前剥离不受支持的 JSON Schema 关键词(`anyOf`/`$ref`/`if`-`then` 等),并将超 64 字符的工具名哈希截断,将流式工具调用名映射回客户端,使 Kiro 不再以 `400 "Improperly formed request"` 拒绝工具调用。([#4847](https://github.com/diegosouzapw/OmniRoute/pull/4847) — 感谢 @smarthomeblack) - **fix(sse):** 使 `anthropic-compatible-*` 服务商的 `anthropic-version` 默认守卫不���分大小写,使调用者/运维人员提供的 `Anthropic-Version`(任意大小写)不再被第二个小写的 `anthropic-version: 2023-06-01` 头覆盖。([#4823](https://github.com/diegosouzapw/OmniRoute/pull/4823) — 感谢 @zakirkun) - **fix(db):** 通过 `whoami-v2` 端点作为纯认证探针校验 HuggingFace API Token,使细粒度 Inference-Provider Token(即使模型/任务端点拒绝它们也有效)不再被错误标记为无效;仅 401/403 表示无效密钥,其他非 OK 状态表现为瞬态上游错误。([#4819](https://github.com/diegosouzapw/OmniRoute/pull/4819) — 感谢 @Delcado19) - **fix(sse):** 在 `buildKiroPayload` 中拒绝仅 Anthropic 的 `[1m]` context-1m 后缀,在其到达 AWS Bedrock 之前阻止 — Kiro 由 Bedrock 支持且无法使用该 beta 功能,因此转发的 `kr/*[1m]` 模型 ID 在上游格式错误;调用者现在获得明确的错误,引导其使用直接 Anthropic 服务商进行 1M 上下文路由。([#4816](https://github.com/diegosouzapw/OmniRoute/pull/4816) — 感谢 @Delcado19) - **fix(dashboard): 使引擎 Combo 编辑器引擎与 API Schema 对齐** — 命名 Combo 管线下拉菜单提供了四个引擎(`headroom`、`session-dedup`、`ccr`、`llmlingua`),而 `PUT /api/context/combos/[id]` 会拒绝它们,因此选择一个会导致保存返回 400,同时 UI 静默吞噬了错误。下拉菜单现在从与 `stackedPipelineStepSchema` 共享的单一标准引擎映射中获取数据(由单元测试守护一致性),编辑器展示保存错误及空名称/空管线校验,而不是静默失败。([#5062](https://github.com/diegosouzapw/OmniRoute/pull/5062) — 关闭 #4955) - **fix(sse):** 将畸形的 HTTP-200 上游响应展示为错误而非视为成功,使 Combo 容灾可以触发。([#4942](https://github.com/diegosouzapw/OmniRoute/pull/4942) — 感谢 @haipham22) - **fix(antigravity):** 对瞬态上游故障进行重试,而非直接使请求失败。([#4941](https://github.com/diegosouzapw/OmniRoute/pull/4941) — 感谢 @Jordannst) - **fix(sse):** 将 WS 桥接控制器关闭错误排除在服务商熔断器之外,使客户端断开连接不再触发整个服务商。([#4870](https://github.com/diegosouzapw/OmniRoute/pull/4870) — 关闭 #4602,感谢 @huohua-dev) - **fix(sse):** 通过 ID 和不区分大小写的名称解析自定义 Combo。([#4869](https://github.com/diegosouzapw/OmniRoute/pull/4869) — 关闭 #4446,感谢 @herjarsa) - **fix(sse):** 在 Responses 翻译器中转发 AI SDK 图像部分。([#4859](https://github.com/diegosouzapw/OmniRoute/pull/4859) — 感谢 @mugnimaestra) - **fix(sse):** 发出有效可拼接的 Kiro `tool_calls.arguments` delta。([#4855](https://github.com/diegosouzapw/OmniRoute/pull/4855) — 感谢 @wahyuzero) - **fix(sse):** 对启用扩展思考的 Claude 模型剥离 `temperature`(上游会拒绝它)。([#4853](https://github.com/diegosouzapw/OmniRoute/pull/4853) — 感谢 @noestelar) - **fix(sse):** 解开 Qoder HTTP-200 SSE 错误信封,使 Combo 容灾可以触发。([#4850](https://github.com/diegosouzapw/OmniRoute/pull/4850) — 感谢 @vianlearns) - **fix(sse):** 从 agentic 上下文中剥离推理块,防止跨多轮代理循环的 O(n²) Token 增长。([#4849](https://github.com/diegosouzapw/OmniRoute/pull/4849) — 感谢 @GodrezJr2) - **fix(sse):** 在 Responses 流中消息内容之前关闭推理块,使客户端按正确顺序渲染推理和回答。([#4848](https://github.com/diegosouzapw/OmniRoute/pull/4848) — 感谢 @kwanLeeFrmVi) - **fix(config):** 将完整的 SiliconFlow 模型列表同步到注册表。([#4844](https://github.com/diegosouzapw/OmniRoute/pull/4844) — 感谢 @letanphuc) - **fix(sse):** 剥离 Composer `<|final|>` 哨兵标记,该标记在 Composer 推理后泄漏。([#4842](https://github.com/diegosouzapw/OmniRoute/pull/4842) — 感谢 @noestelar) - **fix(build):** 在独立打包中追踪包含 `sql.js` 的 `sql-wasm.wasm`,使 SQLite-WASM 在打包构建中正常工作。([#4839](https://github.com/diegosouzapw/OmniRoute/pull/4839) — 感谢 @Delcado19) - **fix(cli):** 将延迟安装的原生运行时依赖(`better-sqlite3`、`systray2`)以 `--save-exact` 而非 `--no-save` 持久化到共享运行时 `package.json`,因此安装一个不再将另一个修剪为"extraneous"——修复了 `--tray` 安装后的 "No SQLite driver available" 故障。([#4841](https://github.com/diegosouzapw/OmniRoute/pull/4841) — 感谢 @omartuhintvs) - **fix(sse):** 在上游调用前将裸模型名解析为连接的 `defaultModel`。([#4825](https://github.com/diegosouzapw/OmniRoute/pull/4825) — 感谢 @anuragg-saxenaa) - **fix(api):** 在服务商节点校验连接错误时展示 Docker localhost 提示。([#4822](https://github.com/diegosouzapw/OmniRoute/pull/4822) — 感谢 @anuragg-saxenaa) - **fix(sse):** 当 Antigravity 信封中存在 `functionDeclarations` 时剥离 Gemini 内置工具(两者在上游互斥)。([#4821](https://github.com/diegosouzapw/OmniRoute/pull/4821) — 感谢 @vanszs) - **fix(sse):** 剥离 `X-Stainless-*` 头并规范化 OpenAI 兼容端点的 SDK `User-Agent`。([#4820](https://github.com/diegosouzapw/OmniRoute/pull/4820) — 感谢 @anuragg-saxenaa) - **fix(oauth):** 允许通过 `providerSpecificData.refreshLeadMs` 进行每个连接的刷新提前量覆盖。([#4818](https://github.com/diegosouzapw/OmniRoute/pull/4818) — 感谢 @anuragg-saxenaa) - **fix(dashboard): 在 `ModelSelectModal` 中通过 `providerId` 解析透传模型别名。([#4815](https://github.com/diegosouzapw/OmniRoute/pull/4815) — 感谢 @anuragg-saxenaa) - **fix(sse):** 从 Antigravity 工具 Schema 中剥离 `enumDescriptions`。([#4813](https://github.com/diegosouzapw/OmniRoute/pull/4813), [#4740](https://github.com/diegosouzapw/OmniRoute/pull/4740) — 感谢 @anuragg-saxenaa) - **fix(dashboard): 通过显式 CSS 类保持桌面侧边栏可见。([#4812](https://github.com/diegosouzapw/OmniRoute/pull/4812) — 感谢 @Delcado19) - **fix(sse):** 在将 Responses API 转换为聊天格式时过滤无名托管工具。([#4789](https://github.com/diegosouzapw/OmniRoute/pull/4789) — 上游,感谢 Владимир Акимов) - **fix(sse):** 流写入器 mock 的 `abort()` 现在返回 Promise(测试稳定性修复)。([#4788](https://github.com/diegosouzapw/OmniRoute/pull/4788) — 感谢 @decolua) - **fix(sse):** 为 Cline 使用 WorkOS 认证 Token 形状。([#4787](https://github.com/diegosouzapw/OmniRoute/pull/4787) — 感谢 @apeltekci) - **fix(api):** 当刷新失败时,为任何 OAuth 服务商回退到现有访问 Token。([#4786](https://github.com/diegosouzapw/OmniRoute/pull/4786) — 感谢 @decolua) - **fix(sse):** 从 `response.usageMetadata` 信封中读取 Antigravity 用量。([#4785](https://github.com/diegosouzapw/OmniRoute/pull/4785) — 感谢 @decolua) - **fix(oauth):** 在自动导入前验证 Linux 上的 Cursor 安装。([#4770](https://github.com/diegosouzapw/OmniRoute/pull/4770) — 上游,感谢 Ibrahim Ryan) - **fix(cli):** 当 `DATA_DIR` 不可写时回退到默认数据目录。([#4767](https://github.com/diegosouzapw/OmniRoute/pull/4767) — 上游,感谢 Thiên Toán) - **fix(sse):** 为不支持结构化输出的 OpenAI 兼容服务商提供 `json_schema` 回退。([#4766](https://github.com/diegosouzapw/OmniRoute/pull/4766) — 感谢 @mustafabozkaya) - **fix(cli):** 在 macOS 自启动中验证 launchd 注册并跳过自身 SIGTERM。([#4765](https://github.com/diegosouzapw/OmniRoute/pull/4765) — 感谢 @ntdung6868) - **fix(sse):** 在 OpenAI Responses 翻译器中,早期流结束时完成 `tool_calls` 的 `finish_reason`。([#4764](https://github.com/diegosouzapw/OmniRoute/pull/4764) — 感谢 @decolua) - **fix(sse):** 在 Claude 能力检查之后才允许 Kiro 图像附件。([#4763](https://github.com/diegosouzapw/OmniRoute/pull/4763) — 感谢 @decolua) - **fix(sse):** 从原始 NDJSON 块跟踪 Ollama 流式用量。([#4754](https://github.com/diegosouzapw/OmniRoute/pull/4754) — 感谢 @fresent) - **fix(sse):** 在 `formatProviderError` 中包含低级原因详情。([#4741](https://github.com/diegosouzapw/OmniRoute/pull/4741) — 感谢 @decolua) - **fix(executors):** `anthropic-compatible-*` 网关现在在 `x-api-key` 之外同时获得 `Bearer` Token。([#4729](https://github.com/diegosouzapw/OmniRoute/pull/4729) — 感谢 @hodtien) - **fix(translator):** 在 claude-to-openai 路径中剥离 `x-anthropic-billing-header`。([#4728](https://github.com/diegosouzapw/OmniRoute/pull/4728) — 感谢 @weimaozhen) - **fix(translator):** 为非 Copilot Responses 客户端保留 `reasoning_effort`。([#4688](https://github.com/diegosouzapw/OmniRoute/pull/4688) — 感谢 @ryanngit / @diegosouzapw) - **fix(codex):** 将 OAuth 401 视为不可恢复的刷新失败(停止重试已失效的 Token)。([#4686](https://github.com/diegosouzapw/OmniRoute/pull/4686) — 感谢 @sacwooky / @diegosouzapw) - **fix(translator):** 在 OpenAI 规范化中将工具描述强制转换为字符串。([#4675](https://github.com/diegosouzapw/OmniRoute/pull/4675) — 感谢 @East-rayyy / @diegosouzapw) - **fix(dashboard): 停止在列表视图中对已脱敏的 API Key 进行二次脱敏(E2E 3/9 回归)。([#4671](https://github.com/diegosouzapw/OmniRoute/pull/4671) — 感谢 @diegosouzapw) - **fix(combo): 展平 Anthropic 工具消息 + 工具历史以防止上游 503。([#4648](https://github.com/diegosouzapw/OmniRoute/pull/4648) — 感谢 @warelik / @diegosouzapw) - **fix(providers): 在兼容服务商 API-key 设置流程中要求设置默认模型。([#4641](https://github.com/diegosouzapw/OmniRoute/pull/4641) — 感谢 @arden1601) ### 🔒 安全 - **fix(auth):** 仅信任来自 loopback TCP 对端的转发头(`X-Forwarded-For` / `X-Real-IP`),使非 loopback 客户端无法伪造来源以绕过仅限本地路由的守卫。([#4689](https://github.com/diegosouzapw/OmniRoute/pull/4689) — 感谢 @Jordannst / @diegosouzapw) - **fix(sse):** 在聊天处理器的 AUTH 调试日志中脱敏 API Key。([#4858](https://github.com/diegosouzapw/OmniRoute/pull/4858) — 感谢 @sacwooky) - **fix(oauth):** 在路由守卫中将 `/api/oauth/cursor/auto-import` 分类为仅限本地路由,使需要 loopback 执行的进程启动端点无法通过隧道/泄漏的 JWT 访问(第 17 号硬规则)。([#5070](https://github.com/diegosouzapw/OmniRoute/pull/5070) — 感谢 @diegosouzapw) ### 📝 维护 - **chore(ci):** 加固发布流程 — 将质量递增门禁与覆盖率分片波动解耦(`if: !cancelled()` + `--allow-missing`),添加快速路径偏差门禁(`check:complexity`、`check:cognitive-complexity`、`check:pack-policy`、`check:build-scope`),并将默认构建堆内存提升至 8 GB。([#5054](https://github.com/diegosouzapw/OmniRoute/pull/5054) — 感谢 @diegosouzapw) - **docs(routing):** 同步 Fusion 的 Combo 策略文档(17 策略)。([#5067](https://github.com/diegosouzapw/OmniRoute/pull/5067) — 感谢 @diegosouzapw) - **test(sse):** 全网关锁定所有服务商的 `provider.ts` 翻译路径。([#4734](https://github.com/diegosouzapw/OmniRoute/pull/4734) — 感谢 @diegosouzapw / @decolua) - **docs(env):** 在 `.env.example` + `ENVIRONMENT.md` 中记录 `HEADROOM_URL`。(感谢 @diegosouzapw) - **chore(quality):** 在 rc17 PR 批次 leva(leva2/leva3/leva4)上重新校准文件大小门禁以吸收周期漂移。(感谢 @diegosouzapw) --- ## [3.8.36] — 2026-06-25 ### ✨ 新功能 **配额共享系统** - **feat(quota):** 引入专用的 `quota-share` Combo 策略(Fase 3 #9) — 带每模型飞行门控(P2C)的 Deficit Round Robin 调度、自动数据库迁移以升级现有 `qtSd/*` Combo,以及每策略门控使无效分配不会将 `allow` 泄漏到非目标连接。([#4939](https://github.com/diegosouzapw/OmniRoute/pull/4939), [#4901](https://github.com/diegosouzapw/OmniRoute/pull/4901)) - **feat(quota):** 多窗口用量桶、每(key,model)上限和会话粘性 — 连接现在跟踪 5 小时、7 天和每模型窗口的消耗;`quota_allocation_model_caps` 强制执行每 key/模型限制;会话粘性在多轮对话中保持提示缓存完整性。([#4928](https://github.com/diegosouzapw/OmniRoute/pull/4928), [#4927](https://github.com/diegosouzapw/OmniRoute/pull/4927), [#4929](https://github.com/diegosouzapw/OmniRoute/pull/4929)) - **feat(quota):** headroom 策略 + 主动饱和度 — 新的 `headroom` Combo 策略按可用配额余量选择连接;通过上游 Token 用量响应头进行通用主动饱和度计算;从 `/api/oauth/usage` 获取真实的 Claude 配额饱和度。([#4908](https://github.com/diegosouzapw/OmniRoute/pull/4908), [#4907](https://github.com/diegosouzapw/OmniRoute/pull/4907), [#4885](https://github.com/diegosouzapw/OmniRoute/pull/4885)) - **feat(quota):** 并发控制 + 冷却等待(Fase 2.1) — `max_concurrent` 在调度时强制执行;quota-share Combo 通过短冷却等待对并发请求进行排队,并在槽位可用时重新调度(方案 A);定时修复任务在窗口重置后主动恢复连接。([#4965](https://github.com/diegosouzapw/OmniRoute/pull/4965), [#4970](https://github.com/diegosouzapw/OmniRoute/pull/4970), [#4967](https://github.com/diegosouzapw/OmniRoute/pull/4967), [#4900](https://github.com/diegosouzapw/OmniRoute/pull/4900)) **Combo 路由** - **feat(combo):** 任务感知路由策略 — 根据任务类型元数据将请求路由到最匹配的连接,实现在 Combo 内的每任务服务商专业化。([#4945](https://github.com/diegosouzapw/OmniRoute/pull/4945)) - **feat(combo):** Fusion 策略(第 16 个策略) — 并行广播到可配置的模型评审团,然后通过裁判模型综合结果。([#4652](https://github.com/diegosouzapw/OmniRoute/pull/4652)) - **feat(combos):** 添加可编辑的每 Combo `description` 字段。路由 Combo 表单现在有 Description 输入框,通过 `/api/combos`(POST/PUT)持久化在 Combo `data` blob 中,并通过 GET 往返 — 无新增数据库列。([#5005](https://github.com/diegosouzapw/OmniRoute/issues/5005)) - **feat(routing):** 遵循 `X-Route-Model` 请求头以覆盖 `body.model`,实现在不修改请求体的情况下进行每请求模型切换。([#4863](https://github.com/diegosouzapw/OmniRoute/pull/4863) — 感谢 @costaeder) **服务商与模型** - **feat(providers):** 更新 volcengine-ark 模型列表,新增 DeepSeek-V4-Flash 和 DeepSeek-V4-Pro。([#4905](https://github.com/diegosouzapw/OmniRoute/pull/4905) — 感谢 @kenlin8827) - **feat(provider):** 新增 CodeBuddy CN(`copilot.tencent.com`)— 完整的 OAuth + 执行器 + 模型目录栈。([#4664](https://github.com/diegosouzapw/OmniRoute/pull/4664)) - **feat(opencode-go):** 通告 `glm-5.2` 和 `kimi-k2.7-code` 以与官方 Go 端点保持一致。([#4711](https://github.com/diegosouzapw/OmniRoute/pull/4711)) - **feat(sse):** 新增 Google Flow 视频生成服务商。([#4769](https://github.com/diegosouzapw/OmniRoute/pull/4769)) - **feat(api/v1):** 在 `/v1/models` 列表中纳入别名支持的模型。([#4630](https://github.com/diegosouzapw/OmniRoute/pull/4630)) **代理池** - **feat(proxy-pool):** Cloudflare Workers 代理部署器 + 池集成 — 直接从控制台部署 Cloudflare Workers 中继并将其注册到代理池中。([#4640](https://github.com/diegosouzapw/OmniRoute/pull/4640)) - **feat(proxy-pool):** Deno Deploy 中继 + 分组操作按钮 — 部署 Deno Deploy 中继工作线程,并使用新的批量操作控件管理代理分组。([#4643](https://github.com/diegosouzapw/OmniRoute/pull/4643)) **压缩与基础设施** - **feat(compression):** Kiro/CodeWhisperer 工具结果压缩引擎 — 专用于 Kiro/CodeWhisperer 工具输出的压缩器,集成到流式传输管线中。([#4635](https://github.com/diegosouzapw/OmniRoute/pull/4635)) - **feat(endpoint):** 每端点自定义系统提示注入。端点设置卡片中的开关 + 文本字段允许用户将自定义系统提示注入到每个模型请求中,通过现有的系统提示引擎应用。存储在设置数据库中。([#5022](https://github.com/diegosouzapw/OmniRoute/pull/5022) — 感谢 @whale9820) - **feat(live-ws):** 通过 `LIVE_WS_ALLOWED_HOSTS` 环境变量允许非回环客户端,使多主机部署可以访问实时 WebSocket API。([#4877](https://github.com/diegosouzapw/OmniRoute/pull/4877) — 感谢 @KooshaPari) - **feat(db):** 在 `usage_history` 上跟踪 API 端点维度,用于每端点成本和用量分析。([#4676](https://github.com/diegosouzapw/OmniRoute/pull/4676)) --- ### 🔧 问题修复 **翻译器** - **fix(translator):** 将并行工具结果重新分组到其原始助手轮次附近,修复对需要严格交错排列的服务商的工具消息排序问题。([#4882](https://github.com/diegosouzapw/OmniRoute/pull/4882)) - **fix(translator):** 在 OpenAI 到 Claude 的流式传输中保留字面空字符串工具参数 — 之前这些参数被丢弃,导致工具调用到达时缺少参数。([#4959](https://github.com/diegosouzapw/OmniRoute/pull/4959)) - **fix(translator):** 将工具规范化为 Anthropic 原生格式,适用于非 Anthropic 服务商,确保工具定义无论调用点的格式如何都能通过校验。([#4650](https://github.com/diegosouzapw/OmniRoute/pull/4650)) - **fix(translator):** 服务商思考兼容性 — 修正 DeepSeek 和 Gemini 服务商的思考块序列化。([#4946](https://github.com/diegosouzapw/OmniRoute/pull/4946)) - **fix(translator):** 为 Anthropic 思考块发出 `` 关闭标记,修复流式响应中推理输出被截断的问题。([#4633](https://github.com/diegosouzapw/OmniRoute/pull/4633)) - **fix(translator):** 将 `developer` 角色规范化为 `system`,适用于 OpenAI 格式的服务商。([#4625](https://github.com/diegosouzapw/OmniRoute/pull/4625)) - **fix(translator):** 在 OpenAI 直通路径上剥离顶层 `client_metadata`(从 9router#1157 移植)。([#4624](https://github.com/diegosouzapw/OmniRoute/pull/4624)) - **fix(translator):** 在纯 Xiaomi MiMo 轮次中重放 `reasoning_content`(从 9router#1321 移植)。([#4639](https://github.com/diegosouzapw/OmniRoute/pull/4639)) **Copilot / GitHub 执行器** - **fix(copilot):** 永远不要将 Gemini/Claude 模型变体路由到 `/responses` 端点 — 这些模型仅需要 chat-completions 路径。([#4627](https://github.com/diegosouzapw/OmniRoute/pull/4627)) - **fix(github):** 将 Copilot Codex 模型路由到 `/responses`(从 9router#102 移植)。([#4626](https://github.com/diegosouzapw/OmniRoute/pull/4626)) - **fix(copilot,antigravity):** 将 `maxOutputTokens` 上限设为 16384,以阻止高令牌请求出现 "Invalid Argument" 400 错误。([#4636](https://github.com/diegosouzapw/OmniRoute/pull/4636)) - **fix(codex):** 丢弃破坏 `responses.stream` 消费者的非标准 `codex.*` 流式事件。([#4715](https://github.com/diegosouzapw/OmniRoute/pull/4715) — 感谢 @jeffer1312) **Claude / Anthropic** - **fix(claude):** 对 Haiku 模型变体省略 `adaptive_thinking` 和 `output_config.effort`,因为这些变体拒绝这些参数。([#4661](https://github.com/diegosouzapw/OmniRoute/pull/4661)) - **fix(claude):** 跳过 `mcp__` 工具名伪装并防范缺失的 `connectionId`,以防止在 Claude 原生 MCP 工具调用时崩溃。([#4861](https://github.com/diegosouzapw/OmniRoute/pull/4861) — 感谢 @costaeder) - **fix(claude-oauth):** 在 Claude OAuth 用量端点上遵循 `429` 退避头,以减少配额检查期间的轮询垃圾请求。([#4655](https://github.com/diegosouzapw/OmniRoute/pull/4655)) **路由与 SSE** - **fix(sse):** 对携带速率限制文本的 `400` 响应进行容灾切换,而不仅仅是对标准的 `429` 状态码。([#4986](https://github.com/diegosouzapw/OmniRoute/pull/4986)) - **fix(sse):** 在 opencode 执行器中遵循每账户代理和指纹轮换设置。([#4989](https://github.com/diegosouzapw/OmniRoute/pull/4989)) - **fix(sse):** 在 auto-combo 评分中对已耗尽的服务商进行软惩罚而非硬排除,改善容灾弹性。([#4990](https://github.com/diegosouzapw/OmniRoute/pull/4990)) - **fix(sse):** 当被钉选的服务商持续不健康时取消 CCP 钉选,并带有防抖动逻辑以防止振荡。([#4864](https://github.com/diegosouzapw/OmniRoute/pull/4864) — 感谢 @costaeder) - **fix(combo):** 从自定义服务商端点动态获取模型,而不是依赖静态列表。([#4860](https://github.com/diegosouzapw/OmniRoute/pull/4860)) - **fix(combo):** 将选定的连接 ID 传播到容灾错误响应,使模型锁定应用于正确的连接而非错误的容灾目标。([#4809](https://github.com/diegosouzapw/OmniRoute/pull/4809) — 感谢 @Chewji9875) - **fix(sse):** 对 Anthropic 原生服务器工具跳过第三方工具名伪装,以防止命名冲突。([#4808](https://github.com/diegosouzapw/OmniRoute/pull/4808) — 感谢 @NomenAK) **配额** - **fix(quota):** 配额独占的 `qtSd/*` 连接现在出现在 `/v1/models` 列表中;EPSILON 阈值检查不再错误地阻止预算不足的分配。([#4830](https://github.com/diegosouzapw/OmniRoute/pull/4830)) - **fix(quota):** 迁移 107 正确地在现有 `qtSd/*` Combo 上激活 `quota-share` 策略。([#4962](https://github.com/diegosouzapw/OmniRoute/pull/4962)) **API / 响应** - **fix(api):** 在热路径上仅解析 `/v1/responses` 请求体一次,而非 3-4 次,减少每请求开销。([#4958](https://github.com/diegosouzapw/OmniRoute/pull/4958)) - **fix(api):** 驱逐过期的内存中速率限制窗口,以阻止长时间运行实例的缓慢堆泄漏。([#4957](https://github.com/diegosouzapw/OmniRoute/pull/4957)) - **fix(api):** 在压缩 `run-telemetry` 端点上要求认证;记录 `OMNIROUTE_EVAL_CREDENTIALS` 环境变量。([#4796](https://github.com/diegosouzapw/OmniRoute/pull/4796)) - **fix(api):** 停止 `GET /api/system/env/repair` 在打包安装时返回 HTTP `500`(它破坏了入门向导)。`createRequire(import.meta.url)` 在模块顶层运行;一旦 webpack 将路由打包到独立构建中,`import.meta.url` 被冻结为构建机器路径,`createRequire` 在求值时抛出异常,因此整个路由加载失败。`createRequire` 现在在受保护的 `better-sqlite3` 块内惰性解析,根目录解析回退到 `process.cwd()`,并且路由传递显式的 `rootDir`。([#5028](https://github.com/diegosouzapw/OmniRoute/pull/5028)) **仪表盘** - **fix(dashboard):** 在仪表盘各页面显示自定义服务商的自定义名称而非内部 ID — 缓存、Combo 健康、压缩分析、成本概览、健康/自动驾驶、服务商统计、路由可解释性、服务商利用率、运行时。添加共享的 `resolveProviderName` 解析器和 `useProviderNodeMap` 钩子。(#4603) - **fix(dashboard):** 在 OAuth 服务商(例如 GLM Coding)上,"测试所有模型" 并自动隐藏失败的模型现在在运行后将模型列表切换到 "可见" 过滤器,使刚隐藏的失败模型在屏幕上真正消失 — 与直通服务商路径保持一致 (#3610)。之前它们在数据库中被隐藏但在 "全部" 过滤器下仍然可见,因此看起来好像没有被隐藏。(#4887) - **fix(dashboard):** 恢复因 #4596 中默认状态变更而被隐藏的首页服务商拓扑卡片。([#4963](https://github.com/diegosouzapw/OmniRoute/pull/4963)) - **fix(dashboard):** 代理池成功门控、同步时间戳持久化以及可选的 Redis 后端。([#4988](https://github.com/diegosouzapw/OmniRoute/pull/4988)) - **fix(dashboard):** 在 LLM 选择器下拉菜单中显示自定义视觉模型。([#4653](https://github.com/diegosouzapw/OmniRoute/pull/4653)) **服务商** - **fix(pollinations):** 停止对每个请求强制启用 `jsonMode`。Pollinations 将 `jsonMode=true` 视为"模型必须返回 JSON"并拒绝(HTTP 400 "messages must contain the word 'json'")任何消息中不包含 "json" 的普通聊天请求,因此所有非 JSON 聊天均被破坏。`jsonMode` 现在仅在调用者实际请求 JSON 输出(`response_format.type` 为 `json_object` 或 `json_schema`)时才启用。(#3981) - **fix(antigravity):** 将 `safetySettings` 默认设为全部关闭,与原生 Gemini 路径保持一致。Antigravity(Google Cloud Code)请求构建器将 `safetySettings` 设为 `undefined`,`JSON.stringify` 会将其丢弃 — 因此没有安全设置到达 Google,其服务器端默认值将无害的技术提示误标记为 `prohibited_content`(HTTP 200 + 被阻止的正文,Combo 容灾将其视为终端错误)。现在会遵循调用者提供的值,否则默认使用 `DEFAULT_SAFETY_SETTINGS`,与 claude-to-gemini / openai-to-gemini 路径保持一致。(#5003) - **fix(antigravity):** 从配额耗尽关键词匹配中排除标准的 Gemini 速率限制消息,以防止误报饱和度标记。([#4810](https://github.com/diegosouzapw/OmniRoute/pull/4810) — 感谢 @Chewji9875) - **fix(chatgpt-web):** 将通告的 `gpt-5.5`、`gpt-5.5-pro`、`gpt-5.4-pro` 和 `gpt-5.2-pro` 目录 ID 映射到其 dash 格式的 ChatGPT 后端标识。它们之前缺失于 `MODEL_MAP`,因此执行器逐字发送点号格式的 ID,ChatGPT 后端静默忽略并返回默认的 Plus 模型而非所请求的模型。添加了一个漂移守卫,断言没有任何通告的点号格式 ID 逐字到达后端。(#4665) - **fix(gemini):** 在 Antigravity 工具 Schema 清理器中保留 `pattern` 字段,以避免从工具定义中剥离有效的正则约束。([#4651](https://github.com/diegosouzapw/OmniRoute/pull/4651)) - **fix(opencode):** 在流式响应中保留 DeepSeek 推理内容。([#4631](https://github.com/diegosouzapw/OmniRoute/pull/4631)) - **fix(perplexity):** 通过 `/v1/models` 端点验证 API 密钥,而非发起完整的聊天请求。([#4654](https://github.com/diegosouzapw/OmniRoute/pull/4654)) - **fix(qoder):** 在发起 Cosy 聊天之前将 PAT 兑换为 `jt-*` 作业令牌,修复 Qoder 凭证格式变更后的认证失败。([#4884](https://github.com/diegosouzapw/OmniRoute/pull/4884)) - **fix(executors):** 剥离目标服务商/模型不支持的参数,以防止在严格端点上出现 `400 Invalid parameter` 错误。([#4658](https://github.com/diegosouzapw/OmniRoute/pull/4658)) - **fix(executors):** 对 Ollama Cloud 保留字面 `reasoning_effort: "max"` 而非规范化为 `xhigh`。Ollama Cloud 接受 `high|medium|low|max|none` 并拒绝 `xhigh`(`invalid reasoning value: 'xhigh'`);OpenRouter DeepSeek 的 `max→xhigh` 规范化不受影响。([#4993](https://github.com/diegosouzapw/OmniRoute/pull/4993) — 感谢 @Thinkscape) - **fix(headroom):** 通过 OpenAI 翻译 openai-responses 输入以进行外部压缩。`adaptBodyForCompression` 现在序列化其 `output` 字段为 JSON 对象(而非字符串)的 `function_call_output` 条目,使压缩引擎能够处理内容 — 之前这些条目因 `hasTextContent()` 对对象值返回 false 而被排除在压缩之外。([#5023](https://github.com/diegosouzapw/OmniRoute/pull/5023) — 感谢 @anki1kr) - **fix(proxy):** 将直接调度器流扇出到所有已注册的代理端点。([#4803](https://github.com/diegosouzapw/OmniRoute/pull/4803) — 感谢 @makcimbx) **压缩** - **fix(compression):** 消除 `math_inline` 保留正则中的 ReDoS — 之前的模式可能在不受信任的输入上发生灾难性回溯。([#4838](https://github.com/diegosouzapw/OmniRoute/pull/4838)) - **fix(compression):** 停止 RTK 过度截断文件读取工具结果 — RTK 现在对文件读取输出尊重完整内容长度。([#4987](https://github.com/diegosouzapw/OmniRoute/pull/4987)) **构建 / CLI / 基础设施** - **fix(build):** 从 `optimizePackageImports` 中移除 `@omniroute/open-sse` 以修复 Next.js 构建 OOM 崩溃。([#4968](https://github.com/diegosouzapw/OmniRoute/pull/4968)) - **fix(cli):** 在关闭 IPC 通道之前 SIGKILL systray 子进程 PID,以防止 macOS NSStatusItem 孤儿进程。([#4732](https://github.com/diegosouzapw/OmniRoute/pull/4732)) - **fix(cli):** 将 `better-sqlite3` 运行时版本锁定提升至 12.10.1 以获得 Node 26 兼容性。([#4685](https://github.com/diegosouzapw/OmniRoute/pull/4685)) - **fix(cli):** 加固 systray2 托盘运行时(从 9router#1080 移植)。([#4628](https://github.com/diegosouzapw/OmniRoute/pull/4628)) - **fix(cli-tools):** 在工具设置文件中容忍 JSONC(注释和尾随逗号)。([#4659](https://github.com/diegosouzapw/OmniRoute/pull/4659)) - **fix(install):** 使 `transformers` 依赖项变为可选,使缺乏 Python 绑定的 CUDA 主机安装能够成功。([#4807](https://github.com/diegosouzapw/OmniRoute/pull/4807) — 感谢 @megamen32) - **fix(db):** 修正存储调优设置以防止高写入负载下 WAL 失控。([#4834](https://github.com/diegosouzapw/OmniRoute/pull/4834) — 感谢 @rdself) - **fix(image):** 防止兼容节点在图像路由表中遮蔽服务商别名。([#4656](https://github.com/diegosouzapw/OmniRoute/pull/4656)) **插件** - **fix(plugin):** opencode `auth.json` 双键容灾以支持自动前缀迁移。配置钩子现在同时查找带前缀的(`opencode-omniroute`)和裸的(`omniroute`)键,因此在 `opencode-` 前缀落地之前认证的用户不再需要重新认证。([#5027](https://github.com/diegosouzapw/OmniRoute/pull/5027) — 感谢 @herjarsa) --- ### 🔒 安全 - **fix(security):** 阻止通过 Deno/Vercel 中继上的 `x-relay-path` 头操纵绕过 SSRF 白名单。([#4899](https://github.com/diegosouzapw/OmniRoute/pull/4899)) - **fix(security):** 固定图像获取 DNS 解析以防止 SSRF DNS 重新绑定攻击(GHSA-cmhj-wh2f-9cgx)。([#4634](https://github.com/diegosouzapw/OmniRoute/pull/4634)) - **fix(security):** 当服务器位于反向代理之后时,不信任回环套接字为仅本地,堵住潜在的认证绕过路径。([#4632](https://github.com/diegosouzapw/OmniRoute/pull/4632)) - **fix(security):** 验证 Kiro 区域参数以防止通过伪造的区域字符串进行 SSRF(GHSA-6mwv-4mrm-5p3m)。([#4629](https://github.com/diegosouzapw/OmniRoute/pull/4629)) - **fix(copilot):** 在 `runOmniRouteCli` 工具中用 `execFile` 替换 `execSync` Shell 插值以防止命令注入。用户提供的命令现在被拆分为 argv 数组并传递给 `execFile`(无 Shell),因此 Shell 元字符被视为字面文本;错误输出通过 `sanitizeErrorMessage()` 路由。([#5024](https://github.com/diegosouzapw/OmniRoute/pull/5024) — 感谢 @hamsa0x7) --- ### 📝 维护 **God-file 分解(持续进行,#3501)** - **refactor(chatCore):** 从 `chatCore.ts` 中提取了 12 个聚焦的辅助函数,涵盖流式管线(`assembleStreamingPipeline`)、缓存存储逻辑(`storeStreamingSemanticCacheResponse`、`storeSemanticCacheResponse`)、响应头(`assembleStreamingResponseHeaders`、`buildNonStreamingResponseHeaders`)、JSON→SSE 桥接(`maybeConvertJsonBodyToSse`)、护栏上下文(`buildPostCallGuardrailContext`)、用量缓冲(`applyClientUsageBuffer`)、插件钩子(`runPluginOnRequestHook`)、分析(`writeCompressionAnalytics`、`emitOutputStyleTelemetry`)以及压缩谓词/设置(`resolveCompressionSettings` 等)。([#4811](https://github.com/diegosouzapw/OmniRoute/pull/4811)–[#4837](https://github.com/diegosouzapw/OmniRoute/pull/4837)) - **refactor(sse/db/api):** 持续分解 `services/usage.ts`(提取了配额核心、标量/格式辅助函数、Antigravity/GLM/MiniMax 用量系列)、`db/core.ts`(Schema-列对账、snake↔camel 列映射)、`db/apiKeys.ts`(行解析器、模型权限匹配)以及 `validation.ts`(URL/请求头/传输叶层、web-cookie/Meta-AI 校验器、企业云 + 探针、音频/语音/API 密钥、搜索/嵌入/重排序以及 OpenAI/Anthropic 格式校验器)。([#4921](https://github.com/diegosouzapw/OmniRoute/pull/4921)–[#4956](https://github.com/diegosouzapw/OmniRoute/pull/4956)) - **refactor(pricing/providers):** 将 `pricing.ts` 分解为共享层级 + 分区的 `DEFAULT_PRICING` 模块,并将 `providers.ts` 目录拆分为按服务商系列组织的语义数据模块。([#4917](https://github.com/diegosouzapw/OmniRoute/pull/4917), [#4918](https://github.com/diegosouzapw/OmniRoute/pull/4918)) - **refactor(open-sse):** 提取 `safeParseJSON` 工具函数并去重 `tryParseJSON` 调用点;提取并去重容灾 `tool_call` ID 生成辅助函数。([#4735](https://github.com/diegosouzapw/OmniRoute/pull/4735), [#4736](https://github.com/diegosouzapw/OmniRoute/pull/4736)) **质量与 CI** - **chore(quality):** 发布基准-红色对账 + 棘轮基准重置 — 跨多个门控的文件大小、环境文档和目录基线更新。([#4630](https://github.com/diegosouzapw/OmniRoute/pull/4630), [#4879](https://github.com/diegosouzapw/OmniRoute/pull/4879), [#4886](https://github.com/diegosouzapw/OmniRoute/pull/4886), [#4915](https://github.com/diegosouzapw/OmniRoute/pull/4915), [#4961](https://github.com/diegosouzapw/OmniRoute/pull/4961), [#4973](https://github.com/diegosouzapw/OmniRoute/pull/4973)) - **ci(quality):** 将重量级校验门控移至 PR→发布合并快速路径,以加速发布周期。([#4857](https://github.com/diegosouzapw/OmniRoute/pull/4857)) - **fix(ci):** 在覆盖率报告产物中包含 `coverage/lcov.info`,以便 SonarQube 可以消费它。([#4670](https://github.com/diegosouzapw/OmniRoute/pull/4670)) - **fix(test):** 通过 `POST /v1/messages` 验证 Anthropic 兼容连接,以获得准确的连接性测试。([#4657](https://github.com/diegosouzapw/OmniRoute/pull/4657)) **文档** - **docs(resilience):** 记录配额共享并发控制 — `max_concurrent` 强制执行、序列化行为以及冷却等待语义。([#4980](https://github.com/diegosouzapw/OmniRoute/pull/4980)) - **docs(perf):** 添加每端点 p50/p95/p99 延迟和成本预算参考。([#4867](https://github.com/diegosouzapw/OmniRoute/pull/4867) — 感谢 @KooshaPari) - **docs(ops):** 添加标准事件响应操作手册。([#4868](https://github.com/diegosouzapw/OmniRoute/pull/4868) — 感谢 @KooshaPari) - **docs(ops):** 记录发布绿色系列 — `green-prs`、`check:release-green`、`babysit` 和夜间门控工作流。([#4679](https://github.com/diegosouzapw/OmniRoute/pull/4679)) - **docs(agentbridge):** 记录 Electron `NODE_EXTRA_CA_CERTS`、真实模型 ID 以及代理桥接集成的身份注意事项。([#4718](https://github.com/diegosouzapw/OmniRoute/pull/4718)) - **docs:** 澄清 Kiro 每账户每月提供约 50 积分,而非无限。([#4690](https://github.com/diegosouzapw/OmniRoute/pull/4690)) **杂项** - **chore(claude,codex):** 提升固定的 CLI 身份版本 — Claude `2.1.158 → 2.1.187`,Codex `0.132.0 → 0.142.0`。([#4883](https://github.com/diegosouzapw/OmniRoute/pull/4883)) - **chore(dashboard):** 将 Qoder 显示标签从 "Qoder AI" 重命名为 "Qoder"。([#4733](https://github.com/diegosouzapw/OmniRoute/pull/4733)) --- ## [3.8.35] — 2026-06-23 ### ✨ 新功能 - **自适应上下文压缩(第四阶段)**:四层压缩升级通过堆叠式 PR 实现——**输出风格**注册表(`terse-prose` / `less-code` / `terse-cjk`)([#4694](https://github.com/diegosouzapw/OmniRoute/pull/4694) — 感谢 @diegosouzapw),可选的 **SLM `ultra` 层级**(两级 LLMLingua 配合启发式容灾)([#4707](https://github.com/diegosouzapw/OmniRoute/pull/4707) — 感谢 @diegosouzapw),**上下文预算自适应拨盘**(预留输出阶梯 + 下限)([#4716](https://github.com/diegosouzapw/OmniRoute/pull/4716) — 感谢 @diegosouzapw),以及**离线评估工具集**(PII 门控语料库、自测评判器、黄金评分器、通过 `ModelClient` 接缝的真实管线运行器)([#4720](https://github.com/diegosouzapw/OmniRoute/pull/4720) — 感谢 @diegosouzapw)。四个层级共享同一 `CompressionRunTelemetry` 契约。 - **Redoc 渲染的 API 文档**:合并后的 OpenAPI 规范现位于 `docs/openapi.yaml`,并作为交互式 Redoc 文档在 `/api/docs` 上提供。([#4781](https://github.com/diegosouzapw/OmniRoute/pull/4781) — 感谢 @KooshaPari / @diegosouzapw) ### 🔧 问题修复 - **db-backups**:通过 `OMNIROUTE_DB_IMPORT_MAX_MB`(默认 100 MB,上限 4 GB)使数据库导入大小上限可配置,以便恢复大于 100 MB 的备份;错误消息现在会指向该环境变量和 VACUUM([#4757](https://github.com/diegosouzapw/OmniRoute/pull/4757) — 关闭 #4719,感谢 @diegosouzapw)。 - **引导流程**:添加缺失的 `onboarding.tiers` 步骤标题翻译,使设置向导不再因 `MISSING_MESSAGE: onboarding.tiers` 而崩溃([#4755](https://github.com/diegosouzapw/OmniRoute/pull/4755) — 关闭 #4698,感谢 @diegosouzapw)。 - **deepseek-web**:将 `role:"tool"` 结果折叠到单提示转录(`messagesToPrompt`)中,使工具输出能到达模型,而不是在后续轮次省略 `tools[]` 数组时被静默丢弃([#4756](https://github.com/diegosouzapw/OmniRoute/pull/4756) — 关闭 #4712,感谢 @diegosouzapw)。 - **控制台**:从 `HomePageClient.tsx` 中移除无效且无条件的 `useLiveRequests()` 调用——它在生产构建中导致 `/home` 页面崩溃并报 `ReferenceError: useLiveRequests is not defined`(#4759, #4745),且即使隐藏了服务商拓扑也会打开 live WebSocket(#4596)。实时订阅现在由设置门控的 `HomeProviderTopologySection` 负责([#4761](https://github.com/diegosouzapw/OmniRoute/pull/4761) — 感谢 @diegosouzapw)。 - **服务商控制台**:添加兼容服务商时按 id 去重(`upsertProviderNodeById`),使同一服务商不再出现两次,且空操作添加不会使兼容服务商备忘录失效([#4768](https://github.com/diegosouzapw/OmniRoute/pull/4768) — 关闭 #4746,感谢 @diegosouzapw)。 - **存储 VACUUM**:定时 VACUUM 任务现在以存储页面设置(`scheduledVacuum` / `vacuumHour`)为唯一数据源;旧的环境变量控制路径已移除([#4726](https://github.com/diegosouzapw/OmniRoute/pull/4726) — 感谢 @rdself)。 - **Tiers**:免认证服务商现在计为免费,免费层级过滤器返回空集合而非穿透到所有服务商([#4753](https://github.com/diegosouzapw/OmniRoute/pull/4753) — 感谢 @megamen32 / @diegosouzapw)。 - **Combos**:自动提升 `zeroLatencyOptimizationsEnabled`,使旧配置(3.8.33 之前的 `fallbackCompressionMode="lite"`)在首次 GUI 编辑时能正常往返([#4774](https://github.com/diegosouzapw/OmniRoute/pull/4774) — 感谢 @KooshaPari / @diegosouzapw)。 ### 📝 维护 - **chatCore (#3501)**:继续将 `executeProviderRequest` 和流式/非流式钩子增量分解为纯叶子模块——顶层辅助函数 + 6 个纯叶子([#4571](https://github.com/diegosouzapw/OmniRoute/pull/4571)),`resolveExecutorWithProxy` + `getExecutionCredentials`([#4646](https://github.com/diegosouzapw/OmniRoute/pull/4646)),Claude 消息转换([#4708](https://github.com/diegosouzapw/OmniRoute/pull/4708)),`persistAttemptLogs`([#4717](https://github.com/diegosouzapw/OmniRoute/pull/4717)),`stageTrace` + `compressionUsageReceipt`([#4721](https://github.com/diegosouzapw/OmniRoute/pull/4721)),`prepareUpstreamBody`([#4730](https://github.com/diegosouzapw/OmniRoute/pull/4730)),解析 + 非流式用量统计([#4762](https://github.com/diegosouzapw/OmniRoute/pull/4762)),`recordContextEditingTelemetryHook`([#4779](https://github.com/diegosouzapw/OmniRoute/pull/4779)),`scheduleQuotaShareConsumption`([#4780](https://github.com/diegosouzapw/OmniRoute/pull/4780)),`emitRequestGamificationEvent`([#4776](https://github.com/diegosouzapw/OmniRoute/pull/4776)),`runPluginOnResponseHook`([#4782](https://github.com/diegosouzapw/OmniRoute/pull/4782)),`scheduleStreamingQuotaShareConsumption`([#4784](https://github.com/diegosouzapw/OmniRoute/pull/4784)),`recordCompressionCacheStats`([#4792](https://github.com/diegosouzapw/OmniRoute/pull/4792)),`writeCavemanOutputAnalytics`([#4794](https://github.com/diegosouzapw/OmniRoute/pull/4794)),`recordStreamingUsageStats`([#4791](https://github.com/diegosouzapw/OmniRoute/pull/4791))和 `recordStreamingCost`([#4790](https://github.com/diegosouzapw/OmniRoute/pull/4790))。(感谢 @diegosouzapw) - **质量**:扩展 `check:release-green` 以在本地复现完整的发布 PR 门控集合([#4758](https://github.com/diegosouzapw/OmniRoute/pull/4758) — 感谢 @diegosouzapw)。 - **db**:从 `localDb` 重新导出 `compressionRunTelemetry` 以满足 db-rules 门控要求([#4775](https://github.com/diegosouzapw/OmniRoute/pull/4775) — 感谢 @diegosouzapw)。 - **安全文档**:添加基于 STRIDE 的规范威胁模型([#4783](https://github.com/diegosouzapw/OmniRoute/pull/4783) — 感谢 @KooshaPari)。 - **测试**:为 home-client 控制台添加冒烟测试([#4793](https://github.com/diegosouzapw/OmniRoute/pull/4793) — 感谢 @JxnLexn)。 - **文档**:在 README 启发项目列表中注明 **ponytail** 和 **OmniCompress** 并修复 `check:env-doc-sync` release-green 检查([#4799](https://github.com/diegosouzapw/OmniRoute/pull/4799) — 感谢 @diegosouzapw);在 README + GUIDE 中声明第四阶段压缩层级([#4801](https://github.com/diegosouzapw/OmniRoute/pull/4801) — 感谢 @diegosouzapw)。 - **质量**:修剪 `combo-config.test.ts` 注释以保持在文件大小上限以下(#4774 的后续工作)([#4800](https://github.com/diegosouzapw/OmniRoute/pull/4800) — 感谢 @diegosouzapw)。 --- ## [3.8.34] — 2026-06-23 ### ✨ 新功能 - **feat(executors): Microsoft 365 Copilot 纯帧封装 + 连接辅助** — 新增请求/响应帧封装和连接辅助,支持 `m365.cloud.microsoft/chat` 用于个人 M365 计划。([#4696](https://github.com/diegosouzapw/OmniRoute/pull/4696) — 感谢 @skyzea1 / @diegosouzapw) - **feat(compression): 按请求的 `x-omniroute-compression` 请求头(第 3 阶段)** — 请求头现在以最高优先级覆盖压缩方案(`请求头 > 路由 > profile > 自动触发 > 默认 > 关闭`),接受 `off` / `默认` / `引擎:` / ``。响应回显 `X-OmniRoute-Compression: ; source=`。([#4645](https://github.com/diegosouzapw/OmniRoute/pull/4645) — 感谢 @diegosouzapw) - **feat(audio): MiniMax T2A v2 文本转语音调度接入 `audioSpeech`** — 新增 MiniMax 文本转语音调度(移植上游 #1043)。([#4553](https://github.com/diegosouzapw/OmniRoute/pull/4553) — 感谢 @diegosouzapw) - **feat(opencode): OpenCode Go DeepSeek 推理变体** — 注册 Go DeepSeek 推理模型变体。([#4647](https://github.com/diegosouzapw/OmniRoute/pull/4647) — 感谢 @DevEstacion) - **feat(quota): OpenCode Go 和 Ollama Cloud 配额抓取** — 展示 OpenCode Go 和 Ollama Cloud 服务商的配额窗口。([#4642](https://github.com/diegosouzapw/OmniRoute/pull/4642) — 感谢 @JxnLexn) - **feat(settings): 暴露流式恢复功能开关** — 在设置中展示流式恢复开关。([#4586](https://github.com/diegosouzapw/OmniRoute/pull/4586) — 感谢 @rdself) - **feat(providers): 自定义 API 密钥校验的可选模型 ID** — 自定义 API 密钥连接测试现在可指定用于校验密钥的模型 ID。([#4555](https://github.com/diegosouzapw/OmniRoute/pull/4555) — 感谢 @diegosouzapw) ### 🐛 问题修复 - **fix(db): 定时清理真正运行 + 查询指向正确的表(数据库膨胀 / OOM)** — `runAutoCleanup` 从未被调度,导致保留清理从未执行,表(`compression_analytics`、`usage_history` 等)无限增长成数 GB 的 SQLite 文件,推高 RSS。更糟糕的是,多个清理查询引用了错误的表名/列名(`call_logs.created_at`→`timestamp`、`compression_analytics.created_at`→`timestamp`、`mcp_audit_log`→`mcp_tool_audit`、`a2a_events`→`a2a_task_events`、`memory_entries`→`memories`),因此即使手动运行也静默无操作或报错。修复了五条查询以匹配真实 schema,新增 `cleanupProxyLogs`,并将 `startCleanupScheduler`(启动时 + 每 6 小时,删除后执行 VACUUM)接入 `server-init`,与现有的预算重置和推理缓存任务并列运行。([#4691](https://github.com/diegosouzapw/OmniRoute/pull/4691),提取自 [#4428](https://github.com/diegosouzapw/OmniRoute/pull/4428) — 感谢 @oyi77 / @diegosouzapw) - **fix(routing): 自动 Combo 纳入所有 noAuth 模型 + 新增 reka-flash + 最佳免费模板** — 构建自动 Combo 时不再跳过 noAuth 服务商模型,注册 `reka-flash`,并新增 `best-free` Combo 模板。([#4621](https://github.com/diegosouzapw/OmniRoute/pull/4621) — 感谢 @oyi77) - **fix: noAuth 服务商校验 + Kimi 执行器路由** — 修正 noAuth 服务商成员检查,移除错误路由的 Kimi 别名。(关闭 #4620)([#4699](https://github.com/diegosouzapw/OmniRoute/pull/4699) — 感谢 @oyi77) - **fix(executors): Firecrawl `web_fetch` 携带 `include_metadata=true` 时返回 500** — 修复 Firecrawl web_fetch 在启用元数据提取时崩溃的问题。([#4692](https://github.com/diegosouzapw/OmniRoute/pull/4692) — 感谢 @ponkcore) - **fix(proxy): 对直连调度器应用 `pipelining:0` + 连接数上限** — 同一服务商的并发请求不再因一个长时/流式传输请求而在直连路径上排队等待。([#4684](https://github.com/diegosouzapw/OmniRoute/pull/4684) — 感谢 @jeffer1312 / @diegosouzapw) - **fix(telemetry): sidecar 不可达时退避实时 WebSocket 事件转发** — 当未配置实时监控时,停止反复尝试连接 `LIVE_WS_PORT`。([#4687](https://github.com/diegosouzapw/OmniRoute/pull/4687) — 感谢 @FikFikk / @diegosouzapw) - **fix(api): `GET /v1/models/{model}` 返回 JSON 而非 HTML 仪表盘** — 按模型端点(通过通配路由支持含斜杠的 ID)现在返回 JSON,修复 Claude Code 兼容性问题。([#4677](https://github.com/diegosouzapw/OmniRoute/pull/4677) — 感谢 @papajo / @diegosouzapw) - **fix(executors): 增强 deepseek-web 工具调用解析和代理上下文保留** — 加固 DeepSeek-web 工具调用解析,并在多轮对话中保留代理上下文。([#4644](https://github.com/diegosouzapw/OmniRoute/pull/4644) — 感谢 @BugsBag) - **fix(cli): `omniroute logs` 认证并遵循当前上下文** — `logs` 命令现在进行认证并遵循当前上下文。([#4638](https://github.com/diegosouzapw/OmniRoute/pull/4638) — 感谢 @Rahulsharma0810) - **fix(stream): 上游报告 `prompt_tokens=0` 时估算输入令牌** — 当上游省略输入令牌用量时进行估算。([#4615](https://github.com/diegosouzapw/OmniRoute/pull/4615) — 感谢 @adivekar-utexas) - **fix(plugin): 为 OpenCode 1.17.8+ 原生网关自动添加 `opencode-` 前缀** — 适配服务商 ID 以对接 OpenCode 1.17.8+ 原生服务商网关。([#4527](https://github.com/diegosouzapw/OmniRoute/pull/4527) — 感谢 @herjarsa) - **fix(catalog): 将无思考网关前缀缩短为 `no-think/`** — 重命名无思考网关前缀。([#4525](https://github.com/diegosouzapw/OmniRoute/pull/4525) — 感谢 @Rahulsharma0810) - **fix(models): 未知最大输出限制不再默认设为 8192** — 没有同步/注册表/静态 `maxOutputTokens` 的模型将限制解析为未知,而非通用的 8192 上限;仅当已知真实上限时才执行钳制/注入。([#4584](https://github.com/diegosouzapw/OmniRoute/pull/4584) — 感谢 @rdself) - **fix(resilience): 遵循上游重试提示开关** — 遵循配置的上游重试提示开关。([#4585](https://github.com/diegosouzapw/OmniRoute/pull/4585) — 感谢 @rdself) - **fix(providers): 显示已保存连接的 API 密钥** — 修复在 UI 中显示已保存连接 API 密钥的功能。([#4583](https://github.com/diegosouzapw/OmniRoute/pull/4583) — 感谢 @rdself) - **fix(logs): 使活跃请求过期清理可配置** — 将过期请求清理间隔暴露为设置项。([#4599](https://github.com/diegosouzapw/OmniRoute/pull/4599) — 感谢 @rdself) - **fix(resilience): 在配置的最大窗口内保留服务商冷却状态** — 冷却状态在配置的最大窗口内持久保留。([#4588](https://github.com/diegosouzapw/OmniRoute/pull/4588) — 感谢 @KooshaPari) - **fix(resilience): 拒绝无效的服务商冷却边界值** — 校验冷却边界配置。([#4589](https://github.com/diegosouzapw/OmniRoute/pull/4589) — 感谢 @KooshaPari) - **fix(combo): 影子淘汰时保留生产 Combo 指标** — 影子淘汰不再丢弃生产 Combo 指标。([#4590](https://github.com/diegosouzapw/OmniRoute/pull/4590) — 感谢 @KooshaPari) - **fix(combo): 自动评分排除已耗尽的连接** — 已耗尽的连接不再被评分为自动 Combo 候选。([#4592](https://github.com/diegosouzapw/OmniRoute/pull/4592) — 感谢 @KooshaPari) - **fix(relay): 对 Bifrost sidecar 应用 IP 速率限制** — 将 IP 速率限制扩展到 Bifrost 中继 sidecar。([#4593](https://github.com/diegosouzapw/OmniRoute/pull/4593) — 感谢 @KooshaPari) - **fix(bifrost): 流结束后完成 SSE 中继用量统计** — SSE 流完成后完成中继用量统计。([#4612](https://github.com/diegosouzapw/OmniRoute/pull/4612) — 感谢 @KooshaPari) - **fix(quota): 展示 Bailian 配额窗口** — 展示 Bailian 服务商配额窗口。([#4610](https://github.com/diegosouzapw/OmniRoute/pull/4610) — 感谢 @KooshaPari) - **fix(dashboard): 首页拓扑实时 WebSocket 网络受小部件可见性控制** — 首页控制台在拓扑隐藏时不再启动拓扑轮询/实时 socket 连接。([#4618](https://github.com/diegosouzapw/OmniRoute/pull/4618), [#4606](https://github.com/diegosouzapw/OmniRoute/pull/4606) — 感谢 @KooshaPari) - **fix(dashboard): 隔离配额小部件刷新时钟** — 配额小部件刷新不再触发无关的重新渲染。([#4611](https://github.com/diegosouzapw/OmniRoute/pull/4611) — 感谢 @KooshaPari) - **fix(dashboard): 缓存兼容服务商分组** — 避免每次渲染时重新计算兼容服务商分组。([#4613](https://github.com/diegosouzapw/OmniRoute/pull/4613) — 感谢 @KooshaPari) - **fix(cli): `omniroute` 数据目录和环境加载与运行时对齐** — CLI 的数据目录/环境加载不再偏离服务器运行时配置。([#4619](https://github.com/diegosouzapw/OmniRoute/pull/4619), [#4607](https://github.com/diegosouzapw/OmniRoute/pull/4607) — 感谢 @KooshaPari) - **fix(api/settings): 防止 `/api/settings` 响应被缓存** — 禁用设置端点的缓存(移植自 9router#951)。([#4566](https://github.com/diegosouzapw/OmniRoute/pull/4566) — 感谢 @diegosouzapw) - **fix(executors): GitHub Copilot gpt-5.4 系列移除 temperature 参数** — 移除 Copilot gpt-5.4 模型不支持的 `temperature` 参数(移植自 9router#612)。([#4564](https://github.com/diegosouzapw/OmniRoute/pull/4564) — 感谢 @diegosouzapw) - **fix(dashboard): 服务商"全部测试"按钮保持 play_arrow 旋转动画** — 修复服务商测试按钮的旋转状态(移植自 9router#715)。([#4563](https://github.com/diegosouzapw/OmniRoute/pull/4563) — 感谢 @diegosouzapw) - **fix(dashboard): Open Claw CLI 自动检测失败时显示手动配置引导** — 在 Open Claw CLI 卡片上自动检测失败时显示手动配置引导按钮。([#4562](https://github.com/diegosouzapw/OmniRoute/pull/4562) — 感谢 @diegosouzapw) - **fix(oauth): 更新 Qwen OAuth URL 从 `chat.qwen.ai` 到 `qwen.ai`** — 刷新 Qwen OAuth 端点(移植自 decolua/9router#683)。([#4561](https://github.com/diegosouzapw/OmniRoute/pull/4561) — 感谢 @diegosouzapw) ### 📝 维护 - **refactor(imageGeneration): 提取 8 个服务商系列到并列文件** — 将图像生成模块拆分为八个并列的按服务商文件,无行为变更。([#4609](https://github.com/diegosouzapw/OmniRoute/pull/4609) — 感谢 @KooshaPari) - **deps: 升级生产 + 开发依赖组;迁移 js-yaml 到 v5 (ESM)** — 依赖升级,外加 `js-yaml` v4→v5 迁移到纯 ESM 命名空间导入。([#4697](https://github.com/diegosouzapw/OmniRoute/pull/4697) — 感谢 @diegosouzapw) - **chore(quality): 发布绿灯预检校验器 + 夜间信号** — 新增 `npm run check:release-green`(`scripts/quality/validate-release-green.mjs`),针对当前工作树重现等效发布的校验(完整单元测试 + vitest + ratchets + typecheck + lint,可选 `--with-build` 构建产物),并将每个红色项分类为 **HARD**(真实缺陷)或 **DRIFT**(ratchet 漂移,发布时重新基线化)——纯诊断性质,永不阻止贡献者。新增 `nightly-release-green` 工作流,在活跃的发布分支上运行该校验,并在硬故障时打开/更新跟踪 issue。填补了完整门禁(`ci.yml`)仅在发布 PR 上运行、导致 `release/**` 上的红色项静默累积并在发布时分批暴露的空白。([#4622](https://github.com/diegosouzapw/OmniRoute/pull/4622) — 感谢 @diegosouzapw) - **chore(quality): 为 #4644 调整文件大小基线(`deepseek-web.ts` 1117→1125)** — deepseek-web 加固后重新基线化文件大小门禁。([#4695](https://github.com/diegosouzapw/OmniRoute/pull/4695) — 感谢 @diegosouzapw) --- ## [3.8.33] — TBD _See English CHANGELOG for v3.8.33 details._ ## [3.8.32] — TBD _See English CHANGELOG for v3.8.32 details._ --- ## [3.8.31] — 2026-06-20 ### ✨ 新功能 - **perf(dashboard): 组合 UI 叶子组件拆分、Next.js 配置调优、一键 Redis 和 Bifrost sidecar** — 交付了 #3932 讨论串中五个性能/UX 轨道中的四个:组合 控制台 页面被拆分为专注的叶子组件(更小的打包体积、更快的重载速度),`next.配置` 针对独立构建进行了调优,Redis 可一键配置,Bifrost sidecar 选项也已接入。(第五个轨道 — chatLogHelpers 提取 — 已在上游完成,因此移除。)([#4381](https://github.com/diegosouzapw/OmniRoute/pull/4381) — 感谢 @KooshaPari) ### 🐛 问题修复 - **fix(embeddings): NVIDIA NIM 非对称嵌入模型注入必需的 `input_type`** — NVIDIA NIM 非对称嵌入模型(如 `nvidia/nv-embedqa-e5-v5`)会拒绝没有 `input_type` 参数的请求,返回 `400 "'input_type' parameter is required"`,但 OmniRoute 仅在客户端提供 `input_type` 时才转发 — 因此调用方(以及不发送该字段的 OpenAI 风格 SDK)会遭遇硬性失败。嵌入注册表现在为 NVIDIA 非对���模型携带模型级默认值(`input_type: "query"`),嵌入处理器仅在客户端未发送时才会将模型默认参数注入上游请求体 — 客户端提供的 `input_type`(如 `"passage"`)将原样保留,而没有默认值的对称模型不受影响。([#4341](https://github.com/diegosouzapw/OmniRoute/pull/4341) — 感谢 @hydraromania) - **fix(api): 将已弃用的 Codex `[features].codex_hooks` 标志迁移到 `[features].hooks`** — Codex 将 `codex_hooks` 功能标志重命名为 `hooks`;最近的 Codex CLI 版本会忽略旧键并打印弃用通知。当 OmniRoute 重写现有的 `~/.codex/config.toml`(配置/重置 Codex 服务商)时,现在会通过重命名 `[features].codex_hooks` → `[features].hooks`(保留其值,绝不覆盖已存在的 `hooks`)并丢弃已弃用的键来保留用户意图。当该标志不存在时无操作。([#4342](https://github.com/diegosouzapw/OmniRoute/pull/4342) — 感谢 @Bian-Sh) - **fix(translator): 同格式响应路径不再泄露 `data: null` SSE 事件** — 流式传输 响应翻译器的同格式快速路径无条件返回 `[chunk]`,因此流结束时的 null/刷新信号(`chunk === null`)被原样传播为 `[null]`。在下游表现为 chunk 之间的空 `data: null` SSE 事件,导致严格客户端(如 Factory Droid BYOK 在 `/v1/responses` 上)崩溃。快速路径现在丢弃 null 刷新(返回 `[]`),同时仍原样传递真实 chunk。([#4344](https://github.com/diegosouzapw/OmniRoute/pull/4344) — 感谢 @thaitryhand) - **fix(translator): 在 OpenAI 目标路径上剥离仅客户端使用的助手回显字段(修复 Mistral 422)** — 严格的 OpenAI 兼容上游(如 `mistral/codestral-latest`)会拒绝作为输入历史发送回去的仅客户端使用的助手"回显"字段,返回 `422 extra_forbidden`(报告中出现了通过 Codex `/responses` 发送的 `messages[].assistant.reasoning_content`)。此前仅在 OpenAI 目标路径上剥离了 `reasoning_content`;同级回显字段 `reasoning`、`refusal`、`annotations` 和 `cache_control` 泄露了出去并触发了 422。现在在非推理 OpenAI 目标路径上全部丢弃。`audio` 被特意保留(OpenAI 音频模型在多轮中通过 id 引用先前的助手音频响应;Mistral 从不发出音频,因此不会有任何损失)。([#4350](https://github.com/diegosouzapw/OmniRoute/pull/4350) — 感谢 @xxy9468615) - **fix(translator): 接受 AI SDK 风格的 `{ type: "image", image: "data:…" }` 内容部分** — 多个 OpenAI 输入翻译器仅识别形状为 `image_url.url`(或带有 `.source`/`.url` 的对象)的图像,因此 AI SDK 风格的部分(其中 `image` 是裸 data-URL 字符串)在到达视觉服务商之前被静默丢弃(OpenCode 是受影响客户端之一;此问题具有普遍性)。OpenAI→Claude、OpenAI→Kiro 和 OpenAI→Gemini/Antigravity 翻译器现在将字符串 `image` data URL 解析为各服务商的原生图像格式(Claude `{source:{type:"base64"}}`、Kiro `images[].source.bytes`、Gemini `inlineData`)。([#4345](https://github.com/diegosouzapw/OmniRoute/pull/4345) — 感谢 @mugnimaestra) - **fix(translator): Gemini 接受 HTTP/HTTPS 图像 URL 而非静默丢弃** — OpenAI→Gemini 请求辅助函数(`convertOpenAIContentToParts`)之前会丢弃远程 `image_url` 部分(仅发出 `console.warn`),因为 Gemini 的 `inlineData` 需要 base64,而同步辅助函数无法获取并编码上游资源。现在对 HTTP/HTTPS URL 使用 Gemini 原生的 `fileData: { fileUri }` 部分(模型自行获取资源),因此携带 URL(而非 `data:` URI)的视觉请求能完整到达 Gemini。([#4373](https://github.com/diegosouzapw/OmniRoute/pull/4373) — 从 9router#344 移植,感谢 @diegosouzapw) - **fix(executors): 为 qwen 非流式/thinking Claude-Code 请求剥离 `stream_options`** — Claude-Code 兼容服务商在 executor 级别强制开启 `stream` 标志,而出站请求体保留了调用方原始的 `stream: false`,因此 `DefaultExecutor.transformRequest` 向仍声明 `stream: false` 的请求体注入了 `stream_options: { include_usage: true }`,qwen 拒绝并返回 `400 "'stream_options' only set this when you set stream: true"`。executor 现在在请求体实际 `stream` 为 false 时剥离 `stream_options`。([#4374](https://github.com/diegosouzapw/OmniRoute/pull/4374) — 从 9router#663 移植,感谢 @anuragg-saxenaa / @diegosouzapw) - **fix(executors): 当 `tool_choice` 强制使用工具时不再注入 `thinking`(原生 Claude)** — Claude-Code 网络镜像模拟会为非 Haiku 的 Claude 模型注入 `thinking: { type: "adaptive" }`,但 Anthropic 在 `tool_choice` 强制使用特定工具(`{type:"any"|"tool"}`)时会拒绝 `thinking`,返回 `400 "Thinking may not be enabled when tool_choice forces tool use."`。任何固定工具的 Opus/Sonnet 调用(例如 Claude Code 的 `message_user`,或强制使用工具的代理工具)都会遇到硬性 400 错误;当 `tool_choice` 强制使用工具时,现在会抑制该注入。([#4389](https://github.com/diegosouzapw/OmniRoute/pull/4389) — 感谢 @NomenAK) - **fix(codex): 在 Codex Responses 请求中请求推理摘要** — Codex/OpenAI Responses 会返回推理 token 统计信息和空推理项,除非请求了可见的推理摘要,因此 Codex CLI / pi.dev 路径会丢失可见的思考文本。OmniRoute 现在在启用推理时请求 `reasoning.summary: "auto"`(并包含 `reasoning.encrypted_content`)— 保留客户端显式设置的 `reasoning.summary` 和现有的 `include` 条目,并在 `reasoning.effort: "none"` 时跳过。([#4359](https://github.com/diegosouzapw/OmniRoute/pull/4359) — 感谢 @xz-dev) - **fix(sse): 将组合的每目标超时默认设为 120s 以实现快速故障转移** — 当组合未设置 `targetTimeoutMs` 时,组合的每目标超时继承了完整的 `FETCH_TIMEOUT_MS`(默认 600s),因此单个挂起/缓慢的目标(例如返回 524/504 的 openai-compatible 上游)可能使**整个**组合停滞长达 10 分钟才能进行故障转移。现在使用新的 `DEFAULT_COMBO_TARGET_TIMEOUT_MS = 120_000` 作为 `resolveComboTargetTimeoutMs` 中未设置时的默认值(向后兼容的第三个参数,在 `phaseComboSetup` 中接入);显式上限/退出选项得以保留。([#4365](https://github.com/diegosouzapw/OmniRoute/pull/4365) — 感谢 @diegosouzapw) - **fix(cli): Tailscale 登录现在遵循 `TAILSCALE_AUTHKEY` 实现非交互式登录** — `startTailscaleLogin` 构建 `tailscale up` 时从未读取 `process.env.TAILSCALE_AUTHKEY`,因此在预先认证/无头守护进程上,登录会等待交互式认证 URL 并超时(约 15s)。当设置了 `TAILSCALE_AUTHKEY` 时,现在通过 `--auth-key=` 传递(作为 spawn argv 元素 — 不进行 shell 插值),守护进程可以非交互式认证;未设置时行为不变。([#4343](https://github.com/diegosouzapw/OmniRoute/pull/4343) — 感谢 @ipeterpetrus) - **fix(dashboard): OAuth 模态框在非 JSON 服务器响应时显示真实错误** — OAuth 连接/重认证模态框无条件调用 `await res.json()`,因此当构建/OAuth 端点返回纯文本错误时(如 `500 Internal Server Error` 页面),模态框抛出 `Unexpected token 'I'…` 并隐藏了真实故障。两个共享辅助函数(`src/shared/utils/api.ts` 中的 `parseResponseBody` / `getErrorMessage`)现在安全地读取响应体(JSON 时按 JSON 解析,否则按原始文本处理),并在任何情况下都呈现清晰的消息;所有模态框的 fetch 调用点都使用了这些辅助函数。([#4351](https://github.com/diegosouzapw/OmniRoute/pull/4351) — 感谢 @DNNYF) - **fix(dashboard): 已禁用连接的最近错误现在可见** — 服务商 卡片's 错误 徽章 counts a disabled 连接 (`isActive === false`) that has an 错误 (its effective status is still 错误/expired/unavailable), but the 连接 row hid the `lastError` text for disabled rows — so the operator saw the 错误 count without being able to see what failed. The row now shows the 错误 text whenever there is one, regardless of the active 开关. ([#4352](https://github.com/diegosouzapw/OmniRoute/pull/4352) — 感谢 @ntdung6868) - **fix(providers): "逐一测试连接" OAuth 探测不再永久卡住队列** — OAuth 连接测试路径调用了裸 `fetch(url, { method, headers })`,没有 `AbortController`/signal/timeout,因此当服务商的探测端点接受 socket 但永不响应时,await 的 fetch 永远不会完成,逐一测试队列会无限期停滞(API-key 路径已通过 `validateProviderApiKey` 的 `timeoutMs` 设置了边界)。现在初始探测和刷新后的重试都设置了 `AbortSignal.timeout(30s)` 边界 — 与 API-key 路径的 30s 预算一致 — 超时探测会以失败形式返回,并附上清晰的 `Test timed out after 30s` 消息,格式与其他测试错误相同。([#4347](https://github.com/diegosouzapw/OmniRoute/pull/4347) — 感谢 @ntdung6868) - **fix(providers): 已停用账户与已吊销 token 被区分标识** — 一个 OAuth 刷新完全正常但 ChatGPT 账户已被服务商停用的 Codex 连接会从上游 API 收到 `401`。连接测试将其与错误凭证等同标记(`Token invalid or revoked` → `upstream_auth_error`),因此操作者无法区分已停用账户和已吊销 token。现在测试会读取 `401`/`403` 的响应体,当表明账户停用时,将其归类为 `account_deactivated` — 控制台已将其呈现为"账户已停用"。普通认证 `401` 保持不变。([#4353](https://github.com/diegosouzapw/OmniRoute/pull/4353) — 感谢 @ntdung6868) - **fix(db): 删除服务商时级联清理孤立的模型别名** — 删除自定义服务商时会移除其连接和节点,但会留下已导入的模型别名行(存储为 `key=`、`value="/<模型>"`)。这些过时的别名随后会阻止重新导入同一服务商 — 导入去重将它们视为"已存在",因此不会出现新的模型。新的 `deleteModelAliasesForProvider(providerId)` DB 辅助函数会删除所有存储值以 `/` 开头的别名(不影响其他服务商和用户定义的 settings 别名),服务商节点 DELETE 处理器现在在移除连接和节点后调用该函数,确保重新导入不受阻碍。([#4348](https://github.com/diegosouzapw/OmniRoute/pull/4348) — 感谢 @nguyenvanhuy0612) - **fix(api): 添加自定义模型时持久化 `max_input_tokens` / `max_output_tokens`** — `POST /api/provider-models` 静默丢弃了"添加自定义模型"表单中设置的每模型 token 限制:处理器解构了请求体的其余部分,但从未读取 `max_input_tokens` / `max_output_tokens`,且 `addCustomModel()` 没有对应参数,因此这些值在写入时被丢弃。DB 层(`inputTokenLimit` / `outputTokenLimit`)和 `/v1/models` 目录已经能对这些字段进行往返 — 只有写入路径缺失。验证 schema 现在接受这两个可选限制,处理器转发它们,`addCustomModel()` 持久化它们,使自定义模型的上下文/输出窗口能保留在目录中。([#4349](https://github.com/diegosouzapw/OmniRoute/pull/4349) — 感谢 @codename-zen) - **fix(plugin): OpenCode 静态目录插件为组合/原始模型键添加服务商 ID 前缀** — OpenCode 的静态目录读取器错误检测了 `omniroute` 服务商:以 `combo/MASTER` 形式发出的组合键被解析为服务商 `combo`("No credentials for provider: omniroute"),而裸 `MASTER` 形式被误读为无法解析服务商的模型,混合的 `omniroute/MASTER` + 裸原始键被 OpenCode 的 schema 拒绝。插件现在为每个组合和原始模型键添加 `omniroute` 服务商 ID 前缀,显式发出服务商 ID,并丢弃旧的 `combo/` 前缀 — 因此静态目录读取器能检测到服务商,认证加载器返回正确的凭证(目录获取超时也已提高,使冷启动服务器不会发布空存根)。([#4384](https://github.com/diegosouzapw/OmniRoute/pull/4384) — 感谢 @herjarsa) ### 🔒 安全 - **fix(security): 将 OAuth 回调的 `postMessage` 限定为可信来源白名单** — `/callback` 处的 OAuth 回调此前在 opener 跨域时向 `window.opener.postMessage(…, "*")` 发送 `{ code, state, … }`,因此恶意页面如果以弹窗形式打开众所周知的回调 URI,就能接收 OAuth code/state 并以用户身份完成流程。通配符回退已被替换为遍历固定白名单(同源 + Codex 的 `localhost:1455` / `127.0.0.1:1455` loopback helper);浏览器会静默丢弃向白名单外的任何 opener origin 发送的 `postMessage`。([#4372](https://github.com/diegosouzapw/OmniRoute/pull/4372) — 从 9router#998 移植,感谢 @aeonframework / @diegosouzapw) - **fix(mitm): 在 MITM 主机测试中使用精确主机匹配(CodeQL 误报)** — `tests/unit/mitm-tool-hosts.test.ts` 使用 `Array.includes(host)` 检查主机成员,CodeQL 的 `js/incomplete-url-substring-sanitization` 启发式规则将其误读为 `String.includes()` URL 子串清理测试(严重误报)。切换为 `.some((h) => h === host)` — 语义相同,且不会触发标志模式。([#4386](https://github.com/diegosouzapw/OmniRoute/pull/4386)) ### 📝 维护 - **docs: 一次性功能文档补全(v3.8.20 → v3.8.30)** — 将所有自 v3.8.20 以来发布的面向用户功能与文档对齐:新增 README **✨ What's New** 章节;新增 [CLI 集成](docs/guides/CLI-INTEGRATIONS.md)、[MITM TPROXY 透明解密](docs/security/MITM-TPROXY-DECRYPT.md)和[代理 Anthropic 上下文编辑](docs/压缩/CONTEXT_EDITING.md)指南;刷新 AUTO-COMBO(`auto/:` + Arena-ELO)、API_REFERENCE(`x-omniroute-no-memory`)、MEMORY(int8 量化,默认关闭)、RESILIENCE(模型锁定成功衰减)、RTK、AGENTBRIDGE、TRAFFIC_INSPECTOR、GUARDRAILS、CLOUD_AGENT、ENVIRONMENT;重新生成 PROVIDER_REFERENCE(231 服务商)并同步 README/CLAUDE/AGENTS 中的服务商数量。今后每次发版都会运行此项(generate-release 步骤 6b)。([#4391](https://github.com/diegosouzapw/OmniRoute/pull/4391)) - **refactor(chatCore): 提取 `checkHeapPressureGuard` 叶子函数(巨型文件分解的开始)** — 分解 `chatCore.ts`(约 5127 行,最热路径 — 每个聊天请求都经过 `handleChatCore`)的第一步。`handleChatCore` 顶部的 V8 堆内存压力守卫(当 `heapUsed` 超过丢弃阈值时拒绝并返回 503)被移至一个独立、同目录的 `utils/heapPressure.ts::checkHeapPressureGuard(...)`,行为无变化。([#4371](https://github.com/diegosouzapw/OmniRoute/pull/4371) — 感谢 @diegosouzapw) - **refactor(combo): 在两个调度器中消除已耗尽目标跳过谓词的重复** — 字节完全相同的 `#1731`/`#1731v2` 预检查(跳过请求中已在服务商/连接上耗尽的目标)存在于两个组合调度器中;已提取为共享的 `Combo/comboPredicates.ts` 辅助函数。([#4362](https://github.com/diegosouzapw/OmniRoute/pull/4362) — 感谢 @diegosouzapw) - **refactor(combo): 在两个调度器中消除上游错误耗尽分类的重复** — 两个调度器都运行了几乎相同的错误后处理代码块,对上游错误进行分类并更新耗尽集合(`#1731` 服务商耗尽 / `#1731v2` 连接错误 / 瞬时速率限制);已提取为共享的 `Combo/targetExhaustion.ts::applyComboTargetExhaustion(...)`。([#4366](https://github.com/diegosouzapw/OmniRoute/pull/4366) — 感谢 @diegosouzapw) - **chore(cli): 本地化 CLI / 抓取文案并稳定 fetch、内存和覆盖率处理** — 本地化 CLI 和抓取 UX 文案以及 Adapta 入门教程(并修正 CLI Code 页面标题),使 fetch 重试遵循启动超时,收紧 SSE/响应类型,在搜索期间遵循配置的内存 token 限制,并通过增量合并 V8 数据减少 CI 覆盖率合并内存开销。([#4383](https://github.com/diegosouzapw/OmniRoute/pull/4383) — 感谢 @JxnLexn) - **test(combo): 在流就绪测试用例之间重置熔断器(恢复绿色状态)** — 一个流就绪回退用例在发版分支上失败,原因是测试隔离问题:同一文件中较早的组合调度用例故意使 `glm` 失败(触发模块级服务商熔断器),该 OPEN 状态泄露到了下一个测试,因此 `Combo.ts` 跳过了模型。测试现在在用例之间重置熔断器。([#4396](https://github.com/diegosouzapw/OmniRoute/pull/4396) — 感谢 @diegosouzapw) - **chore(quality): 对齐复杂度基准线(1896 → 1900)** — 将 v3.8.31 `/review-prs` 合并批次带来的小幅复杂度指标增长吸收到 `quality-baseline.json` 中,使基准线反映已发布的代码(无生产变更)。([#4410](https://github.com/diegosouzapw/OmniRoute/pull/4410) — 感谢 @diegosouzapw) - **test/gate: 对齐完整 CI 门禁发现的发版时漂移** — 三个已合并的变更导致发版分支的完整 CI 门禁变红(各 PR 的快速门禁不会运行):Gemini `convertOpenAIContentToParts` 测试已与 [#4373](https://github.com/diegosouzapw/OmniRoute/pull/4373) 的 HTTP/HTTPS-URL `fileData` 透传对齐(它们之前仍断言旧的 warn-and-drop 行为),`open-sse/executors/base.ts` 的 `t11` any 预算提升至 2 并附有理由([#4389](https://github.com/diegosouzapw/OmniRoute/pull/4389) 将 `tool_choice` 与字符串字面量 `"any"` 比较,而非 TS `any` 类型),[#4384](https://github.com/diegosouzapw/OmniRoute/pull/4384) 的 opencode 插件组合测试的网络断言减少(删除已废弃的 `Combo/` 命名空间)已加入白名单。无生产行为变更。(感谢 @diegosouzapw) --- ## [3.8.30] — 2026-06-20 ### ✨ 新功能 - **feat(dashboard): 服务商页面的媒体类别 (media serviceKind) 筛选器** — `/dashboard/providers` 新增媒体类别筛选行(Image / Video / Music / Text→Speech / Speech→Text / Embedding),与现有的搜索、仅显示免费和"仅显示已配置"筛选器组合使用。类别归属从后端媒体注册表推导(即使服务商未声明 `serviceKinds`,只要它实际提供某种媒体服务就会被列出),确保 UI 与后端保持同步。([#4240](https://github.com/diegosouzapw/OmniRoute/issues/4240)) - **feat(combo): 按步骤的账户允许列表 — 将轮询/加权步骤范围限定到服务商连接的子集** — Combo 模型步骤现在可以携带一等公民的账户允许列表,这样轮询(或加权)策略可以限定到服务商连接的一个选定子集(例如只选择 `foo1`+`foo2` 而不是 `foo1..foo4`),而无需为每个账户手动固定一个步骤。留空 = 使用全部活跃池(行为不变)。当步骤同时具有允许列表和标签路由时,两者取交集(最严格者胜);单个固定账户仍然优先。Combo 构建器的 Precision 步骤编辑器新增可选的"限制到特定账户"选择器。([#3266](https://github.com/diegosouzapw/OmniRoute/issues/3266)) - **feat(providers): 新增 OpenAdapter、dit.ai 和 TokenRouter 作为 OpenAI 兼容服务商** — 三个社区请求的 OpenAI 兼容聚合器现在注册为标准命名的 OpenAI 风格服务商,支持实时 `/v1/模型` 发现(zenmux 模式),在上游列表不可用时回退到种子目录:**OpenAdapter**(`https://api.openadapter.in/v1`,免费层级,70+ 开源模型 — [#4239](https://github.com/diegosouzapw/OmniRoute/issues/4239))、**dit.ai**(`https://api.dit.ai/v1`,动态定价路由器/网关 — [#4155](https://github.com/diegosouzapw/OmniRoute/issues/4155))、**TokenRouter**(`https://api.tokenrouter.com/v1`,免费 MiniMax 模型 — [#3841](https://github.com/diegosouzapw/OmniRoute/issues/3841),感谢 @FerLuisxd)。无需自定义执行器/翻译器 — 默认 OpenAI 直通。 - **feat(api): `x-omniroute-no-memory` 请求头 — 按请求选择退出记忆/技能注入** — 自行管理上下文的客户端(例如自有的 RAG/记忆系统)可以发送 `x-omniroute-no-memory: true`(沿用现有的 `x-omniroute-no-缓存` 约定),跳过网关将最多 `memorySettings.maxTokens`(约 2k)tokens 的记忆**和**技能上下文注入到该聊天请求中——避免每次调用带来的 token/成本膨胀。不发送此头时行为不变。(PRD-2026-06-19-no-memory-头) - **feat(dashboard): MITM 工具卡片列出需要手动添加的完整 hosts 文件条目** — CLI 工具的 MITM 卡片中"工作原理"部分现在列出所选工具的完整 `127.0.0.1 ` 行集合(源自规范的 MITM 目标注册表),而非仅显示单个示例域名。在锁定机器上 — 即自动的、需要 sudo 权限的 hosts 文件编辑不可用时 — 用户现在可以手动复制每个必需条目。(感谢 @mrcyclo) - **feat(cli): `omniroute launch-codex` + `setup-codex` — 运行/配置 Codex CLI 对接 OmniRoute** — 启动器和设置命令,将 Codex CLI 指向 OmniRoute 端点(支持远程模式)。([#4270](https://github.com/diegosouzapw/OmniRoute/pull/4270)) - **feat(cli): Claude Code 启动器 + 设置 — 远程模式 + 配置文件** — `omniroute launch`/`setup` 用于 Claude Code,支持远程模式和命名连接配置文件。([#4274](https://github.com/diegosouzapw/OmniRoute/pull/4274)) - **feat(cli): OpenCode 设置 — OpenAI 兼容服务商 + 远程感知插件** — `setup-opencode` 将 OmniRoute 注册为 OpenCode 的 OpenAI 兼容服务商,并安装远程感知插件。([#4277](https://github.com/diegosouzapw/OmniRoute/pull/4277)) - **feat(cli): 流行 AI 编码工具的一键设置命令** — 新增 `setup-*` 命令,配置各工具对接 OmniRoute:**Cline** ([#4280](https://github.com/diegosouzapw/OmniRoute/pull/4280))、**Kilo Code** ([#4284](https://github.com/diegosouzapw/OmniRoute/pull/4284))、**Continue** ([#4289](https://github.com/diegosouzapw/OmniRoute/pull/4289))、**Cursor** ([#4291](https://github.com/diegosouzapw/OmniRoute/pull/4291))、**Roo Code** ([#4292](https://github.com/diegosouzapw/OmniRoute/pull/4292))、**Crush** ([#4298](https://github.com/diegosouzapw/OmniRoute/pull/4298))、**Goose** ([#4300](https://github.com/diegosouzapw/OmniRoute/pull/4300))、**Qwen Code** ([#4301](https://github.com/diegosouzapw/OmniRoute/pull/4301))、**Aider** ([#4302](https://github.com/diegosouzapw/OmniRoute/pull/4302)) 以及 **Gemini CLI**(原生 `/v1beta`)([#4303](https://github.com/diegosouzapw/OmniRoute/pull/4303))。 - **feat(providers): 服务商模型全面扫描 — 实时发现、刷新目录、清理失效服务商** — 一次大规模扫描,为更多 OpenAI 风格服务商启用实时 `/v1/模型` 发现(zenmux 模式),用最新模型刷新种子目录,并将失效服务商标记为 `deprecated`。([#4324](https://github.com/diegosouzapw/OmniRoute/pull/4324)) - **feat(mitm): 端到端翻译 Antigravity cloudcode 流量(Gap B)** — MITM 解密路径现在端到端翻译 Antigravity `cloudcode` 流量。([#4299](https://github.com/diegosouzapw/OmniRoute/pull/4299)) - **feat(keys): 按 API 密钥的美元使用配额控制** — API 密钥现在可以携带美元消费配额,在达到阈值后限制其使用。([#4327](https://github.com/diegosouzapw/OmniRoute/pull/4327) — 感谢 @Witroch4) ### 🔧 变更 - **change(memory): 记忆功能现在默认关闭** — `DEFAULT_MEMORY_SETTINGS.enabled` 现在默认为 `false`。启用记忆功能会向**每个**聊天请求注入最多约 2,000 个 token 的检索上下文(该上下文会被计费),这对于新安装和拥有自己上下文的客户端来说是一个意外的默认行为。记忆功能现在需要显式选择启用:已启用它的安装保持不变;从未配置过的安装默认为关闭。设置 → 记忆面板现在在启用记忆功能时显示 token 成本警告。(PRD-2026-06-19-no-memory-header) ### 🐛 问题修复 - **fix(translator): Gemini 接受 HTTP/HTTPS 图片 URL(不再静默丢弃)** — OpenAI 风格的 `image_url` 部分,当 URL 为 `http://…` 或 `https://…` 时,到达 `convertOpenAIContentToParts`(OpenAI→Gemini 请求助手)后仅输出 `console.warn` 就被丢弃,因为 Gemini 的 `inlineData` 需要 base64 编码,而该助手是同步的(无法获取并编码)。然而 Gemini 的 `Part` 模式原生支持远程 URI 的 `fileData: { fileUri }` — 模型会自行获取资源。该助手现在生成 `fileData` 部分(`mimeType: "image/*"`,在上游获取时推断)而不是丢弃,因此传递 URL(而非 data: URI)的视觉请求现在可以完整到达 Gemini。`data:` URI 仍然通过 `inlineData` 不变处理;不支持的协议(如 `ftp:`)仍然跳过。(感谢 @East-rayyy) - **fix(security): OAuth 回调页面不再将 `code`/`state` 中继到通配符 `postMessage` 目标** — `/callback` 的 OAuth 回调在跨域打开窗口时(远程控制台 + 本地回环回调场景的容灾机制)将 `{ code, state, ... }` 发送到 `window.opener.postMessage(..., "*")`。因此,一个恶意的页面如果在已知的重定向 URI 的弹窗中打开回调 URL,就会收到 OAuth code+state,从而可以以用户身份完成 OAuth 流程。通配符容灾机制被替换为遍历固定的可信目标源允许列表(同源 + Codex 的回环助手 `localhost:1455` / `127.0.0.1:1455`);浏览器对于源不在列表中的任何打开窗口静默丢弃消息。方法 2(`BroadcastChannel`)和方法 3(`localStorage`)— 已存在于页面中 — 在 COOP 切断打开窗口关系时仍然覆盖同源父窗口。(感谢 @aeonframework) - **fix(compliance): 启动清理现在遵循控制台数据保留设置,而非总是修剪为 7 天** — 每次重启时,`cleanupExpiredLogs()`(启动时运行)仅从 `CALL_LOG_RETENTION_DAYS` / `APP_LOG_RETENTION_DAYS` 环境变量读取保留时间,未设置时默认为 **7 天**,并在控制台驱动的 `runAutoCleanup()`(遵循配置的保留设置)运行之前就修剪了 `usage_history`(使用分析数据)。因此控制台设置的"数据保留" 90 天被静默覆盖,使用分析页面在重启后只显示最近 7 天的数据。保留时间现在遵循优先级 **显式环境变量 → 控制台 DB 设置 → 7 天默认**,按表区分(`usage_history`→`usageHistory`、`call_logs`/`proxy_logs`/`request_detail_logs`→`callLogs`、`mcp_tool_audit`→`mcpAudit`);设置环境变量的操作员仍然胜出,非 DB 部署仍然回退到它。([#4354](https://github.com/diegosouzapw/OmniRoute/issues/4354) — 感谢 @akbardwi) - **fix(providers): bailian-coding-plan 静态回退目录与注册表匹配(10 个模型)** — 服务商-模型扫描(#4324)将四个当前的 Model Studio coding-plan 模型(`qwen3.7-plus`、`qwen3-coder-plus`、`qwen3-coder-next`、`glm-4.7`)添加到了 `bailian-coding-plan` 注册表条目中,但遗漏了 `staticModels.ts` 中的静态容灾镜像,后者仍然只列出了旧的六个。因此静态目录(在实时发现不可用时提供服务)与注册表不一致,现有的 static↔registry 一致性测试在发布分支上变红(仅在测试影响分析恰好选中它时才暴露)。静态镜像现在按注册表顺序包含全部十个模型,恢复了一致性。([#4324](https://github.com/diegosouzapw/OmniRoute/pull/4324)) - **fix(executors): ArenaLLM 适配 LMArena 的分片 Supabase SSR 认证 Cookie** — LMArena 迁移到了 `@supabase/ssr` 分片认证 Cookie:单个 `arena-auth-prod-v1` Cookie 现在为空,实际会话被分割到 `arena-auth-prod-v1.0`、`arena-auth-prod-v1.1`……(递增)中。用户粘贴(现在为空的)单个 Cookie 时,因此发送了空会话,上游以"无效 Cookie"拒绝。LMArena 执行器现在从其分片中重建单个 Cookie——按递增数字顺序读取 `.0`、`.1`……直到缺失为止,并将它们的原始值拼接起来(`@supabase/ssr` 的 `combineChunks` 规则:纯 `join("")`,不进行 base64 解码,不进行 JSON 解析,`base64-` 前缀原样保留)——同时保留粘贴的 Cookie jar 中的其余内容。非空的单个 Cookie 仍然原样转发(向后兼容)。凭证 UX 现在指示粘贴**完整的 Cookie 请求头**并追踪 `.0`/`.1` 存储键。([#4271](https://github.com/diegosouzapw/OmniRoute/issues/4271) — 感谢 @caussao) - **fix(compression): 为自动缓存服务商保留可缓存前缀** — OpenAI / Codex(以及 Azure-OpenAI)使用 _自动_ 前缀缓存:上游缓存请求的最长匹配前缀(系统提示 + 最早的消息),**无需**在请求体中添加任何显式的 `cache_control` 标记。缓存感知的压缩守卫仅在请求携带显式 `cache_control` 时保护该前缀,因此对于自动缓存服务商,该守卫被跳过——而在启用压缩且 `preserveSystemPrompt: false`(或使用 `aggressive`/`ultra` 等前缀压缩模式)时,它会重写系统提示 / 最早的消息,导致必然的缓存未命中,通过 OmniRoute 的令牌消耗**反而高于**直连。守卫现在将缓存服务商本身视为充分条件(仅凭 `isCachingProvider`,不依赖 `cache_control`)来跳过系统提示并降级前缀压缩模式,且 OpenAI/Codex/Azure 现在被识别为缓存服务商。压缩功能默认仍然关闭——这仅影响已启用压缩且关闭了前缀保留的操作者。([#3955](https://github.com/diegosouzapw/OmniRoute/issues/3955)) - **fix(executors): DuckDuckGo AI Chat 使用 duckduckgo.com(修复 400 错误)** — DuckDuckGo AI Chat 执行器之前向 `https://duck.ai` 获取状态/聊天并设置 `Origin`/`Referer`,同时仍然发送 `Sec-Fetch-Site: same-origin`,因此请求的同源三元组(主机 + Origin + Referer)不一致,后端以 HTTP 400 拒绝。所有当前的 DDG 逆向工程参考——以及服务商注册表自身的 `baseUrl`——均使用 `https://duckduckgo.com`;执行器现在在状态 URL、聊天 URL、`Origin` 和 `Referer` 中一致使用它(同源请求头现在保持一致)。`x-fe-version` 抓取正则之前要求 40 位十六进制尾部,但实际提供的令牌具有 20 位十六进制尾部(例如 `serp_20250401_100419_ET-19d438eb199b2bf7c300`),因此它静默回退到硬编码的默认值;模式放宽为有界的 `{20,40}` 尾部(仍然 ReDoS 安全)。此修复解决了报告中的 DuckDuckGo 部分;独立的 Chipotle/`chipotle` 上游故障将另行跟踪。([#4037](https://github.com/diegosouzapw/OmniRoute/issues/4037) — 感谢 @daniij) - **fix(security): 将提示注入扫描限制在前 16 KB(热路径性能优化)** — 提示注入守卫之前将每条消息/系统字符串合并到一个缓冲区中,并在每次聊天请求时对整个内容运行多个正则表达式,没有大小上限——因此 300 KB 的请求体(粘贴的代码、RAG 上下文)意味着在热路径上进行 O(body) CPU 扫描,成为并发场景下自我造成的延迟/GC 来源。两个检测调用点(`inputSanitizer.ts` 中的 `detectInjection` 和 `promptInjection.ts` 中的自定义模式扫描)现在在正则循环之前将合并文本截取到前 **16 KB**(`MAX_INJECTION_SCAN_BYTES`)。注入指令位于提示的顶部附近,因此慷慨的上限在仅扫描有界前缀的同时保留了真正的检测能力;现有的 10 MB 请求体大小上限(保护摄入)保持不变。([#3932](https://github.com/diegosouzapw/OmniRoute/issues/3932) — 感谢 @KooshaPari) - **fix(sse): 在新 Socket 上重试直连 Socket 失败(减少 `502` 突发)** — 默认直连 undici 调度器池将 keep-alive Socket 保留最多 4 秒,但某些边缘节点(例如 `nvidia`、`opencode-zen`)在该窗口内静默关闭空闲的 keep-alive Socket,因此下一个重用池化 Socket 的请求会失败并报 `UND_ERR_SOCKET`("对方已关闭")——且是突发性的。`proxyFetch` 已经在此类瞬时错误上重试一次,但重试复用了**同一个**池化调度器,可能抓取到另一个过期 Socket,然后落到原生 fetch(也是池化的)→ 作业在速率限制队列中等待直到 30 秒超时 → `502` + 熔断器打开。重试现��使用专用的 **no-keep-alive / no-pipelining** 调度器,因此它打开一个全新的 Socket,不可能是一个已死的池化 Socket;第一次尝试仍然使用池化调度器(保留健康的 keep-alive 复用)。与 v3.8.29 的诊断功能(`describeFetchCause`,#4281)互补。([#4252](https://github.com/diegosouzapw/OmniRoute/issues/4252) — 感谢 @klimadev) - **fix(sse): Combo 现在在遇到首个请求体特定的 400 错误时停止,而非尝试每个目标** — 检测请求体特定 400(上下文溢出 / 格式错误 / 模型访问被拒,例如"使用 Codex 与 ChatGPT 账户时不支持该模型")的 `#2101` 守卫记录了"停止 Combo"但执行了一个裸 `break`,这仅退出了内部重试循环;`executeTarget` 随后返回 `null`,外部目标循环将其视为"此目标未产生任何内容"并推进到下一个模型。因此,一个所有 N 个目标都拒绝相同请求体的 Combo 会遍历全部 N 个目标(报告中显示一个 143 模型的 Codex Combo 迭代了每个目标),浪费了上游调用和每次尝试的工作。守卫现在通过 `{ ok, 响应 }` 契约(镜像 499 客户端断开路径)将 400 上浮,使 Combo 立即解析并停止。([#4279](https://github.com/diegosouzapw/OmniRoute/issues/4279)) - **fix(sse): 通过 Responses-API 目标的非流式 Combo 不再返回空内容** — Responses-API 目标(codex/`cx`)即使在 `流:false` 时也从上游流式传输,其终端 `响应.completed` 快照可能携带一个非空的 `output`,但缺少助手消息项(例如仅有一个 `reasoning` 项),而流式传输的 `output_text` delta 已重建了完整消息。SSE→JSON 聚合器之前优先选择终端 `output` 整体,丢弃了重建的文本 → HTTP 200 但内容为空(尤其通过 n8n 触发,n8n 默认 `流:false`)。聚合器现在在终端输出没有消息项但重建有内容时回退到重建的 delta 输出;终端快照在已经携带消息时仍然优先。([#3948](https://github.com/diegosouzapw/OmniRoute/issues/3948)) - **fix(executors): 保留原生 Claude OAuth 的工具名称大小写(`read` 不再泄漏为 `Read`)** — 原生 Claude OAuth 流量经过一个反指纹工具名称伪装,该伪装将字面名为 `read` 的工具在线上重命名为 `Read`,并在一个不可枚举的 `_toolNameMap` 上记录反向别名,响应端使用它来恢复客户端的原始大小写。自 v3.8.27 起,执行器返回请求体的 JSON 往返副本作为 `transformedBody`,该往返丢失了不可枚举的映射——因此恢复时看到空映射,伪装的 `Read` 原样流式传输给客户端,损坏了工具名称。执行器现在将伪装映射重新附加到序列化请求体上(镜像 Antigravity 执行器),因此工具名称大小写正确往返。([#4307](https://github.com/diegosouzapw/OmniRoute/issues/4307) — 感谢 @dev-cj) - **fix(api): 缓存命中时 `X-OmniRoute-Response-Cost` 现在报告增量成本(≈0),而非原始成本** — 在语义缓存命中时,网关在**没有**上游调用的情况下提供存储的响应,但 `X-OmniRoute-Response-Cost` 报告的是原始调用的完整成本(从缓存的 `usage` 重新计算)。因此,对 `响应-成本` 求和的计费消费者在向服务成本 ≈$0 的响应收费(且过期条目可能夸大金额)。缓存命中现在计费 `X-OmniRoute-Response-Cost: 0.0000000000`(真正的增量成本),而避免的成本通过一个新的 **`X-OmniRoute-Cost-Saved`** 请求头暴露,用于缓存分析——镜像现有的 `tokens_saved` 概念。MISS 路径保持不变。(PRD-2026-06-19-缓存-hit-成本-reporting) - **fix(models): 导入的视觉能力模型保留其视觉能力** — 导入服务商密钥后,视觉能力模型(例如 `architecture` 声明图像输入的 OpenRouter 模型,以及其他同步的服务商)在 `/v1/模型` 和控制台中被列为纯文本——尽管图像请求实际上可以工作。同步的模型记录从未捕获视觉标志,而目录的 OpenRouter 实时增强(从 `architecture.input_modalities` 推导视觉能力)在服务商有同步模型后会被跳过。发现过程现在在同步时捕获 `supportsVision`(从 `architecture.input_modalities`、字符串 `architecture.modality` 或顶层 `input_modalities`),镜像现有的 `supportsThinking` 捕获,且目录为同步模型展示 `capabilities.vision`。([#4264](https://github.com/diegosouzapw/OmniRoute/issues/4264) — 感谢 @FerLuisxd) - **fix(providers): Cloudflare Workers AI 模型发现显示模型名称而非 UUID** — 导入 Cloudflare Workers AI 密钥时,模型以内部 UUID 标识符列出(例如 `429b9e8b-d99e-…`)而非其可用别名(`@cf/meta/llama-3.1-8b-instruct`)。Cloudflare 的 `/ai/模型/搜索` 返回 `{ id: "", name: "@cf/…" }`,发现过程直接将原始对象传递——因此 UUID `id` 成为了可调用的模型 ID。`cloudflare-ai` 发现现在将每个结果的 `name` → id 进行映射,展示真实的 `@cf/…` 模型 ID。([#4259](https://github.com/diegosouzapw/OmniRoute/issues/4259) — 感谢 @FerLuisxd) - **fix(translator): 将 Responses API `call_id` 限制在 64 个字符** — OpenAI Responses API 拒绝长度超过 64 个字符的 `call_id` 值并返回 400。长上游工具调用 ID(某些客户端发出的 ID 远超此限制)现在在 `function_call` 项及其匹配的 `function_call_output` 上确定性截断,使配对通过孤立输出过滤器保持匹配且请求被接受。(感谢 @anuragg-saxenaa, @ngapngap) - **fix(oauth): GitHub Copilot 令牌刷新现在发送公共 client_id** — `github` 服务商配置从未携带 `clientId`,因此 GitHub OAuth `refresh_token` 交换要么省略了 `client_id`,要么发送了字面字符串 `undefined`(以及伪造的 `client_secret=undefined`),GitHub 会拒绝——导致 Copilot 连接在其短期令牌过期且需要长期刷新路径时卡住。服务商现在从嵌入的公共凭证中解析其公共 device-flow `client_id`,并完全省略 `client_secret`(GitHub 的 Copilot 应用是没有 secret 的公共客户端)。(感谢 @baslr) - **fix(translator): 名为 `pattern` 的工具属性在 Gemini/Antigravity Schema 清理后得以保留** — Gemini Schema 清理器在每个嵌套层级剥离 Gemini 拒绝的 JSON-Schema 约束关键字(`pattern`、`minLength`……),但它也删除了任何字面上以这些关键字**命名**的工具**属性**。glob/grep 工具声明了一个名为 `pattern` 的属性,因此在 `ag/*`(Antigravity)后端上,该参数(及其 `required` 条目)被静默丢弃,破坏了工具。关键字剥离现在是位置感知的:它仅在 Schema 节点级别移除约束关键字,而不会触及 `properties` 映射内的用户定义名称。真正的字符串级别 `pattern` _约束_ 仍然会被剥离。(感谢 @youthanh) - **fix(translator): MCP `namespace` 工具在 Responses→Chat 路径上展开为单独函数** — 当 Codex CLI 客户端将 Responses-API 请求路由到非 Codex 后端(例如 `kr/claude-opus-4.7`)时,每个 MCP 服务器被声明为一个 `namespace` 工具(`{ type:"namespace", name, tools:[…] }`)。Responses→Chat 翻译器没有 `namespace` 分支,因此整个组折叠为一个名为 `mcp____` 的空 Schema 函数,每次 MCP 调用都返回 `unsupported call: mcp____`,破坏了该组合的所有基于 MCP 的工作流(context7、codegraph、自定义 MCP)。翻译器现在将命名空间展开为每个子工具一个 Chat 函数(保留每个子工具的名称和参数);空命名空间不产生任何工具而非产生一个损坏的占位符。原生 Codex 直通路径此前已经正确。(感谢 @V13t4nh) - **fix(cli): 活跃的远程上下文凭证优先于环境中的 `OMNIROUTE_API_KEY`** — 当选择了远程上下文时,其范围限定的访问令牌现在优先于环境中存在的 `OMNIROUTE_API_KEY`,使连接的远程目标按预期被选中。([#4364](https://github.com/diegosouzapw/OmniRoute/pull/4364)) - **fix(cli): 将 `contexts` 命令接入 CLI 程序** — `omniroute contexts` 命令(列出/切换已保存的远程上下文)已实现但从未注册,因此无法访问;现已接入 CLI 程序。([#4369](https://github.com/diegosouzapw/OmniRoute/pull/4369)) - **fix(mitm): 在流量检查器中屏蔽裸 `Bearer ` 请求头值** — 检查器现在遮盖裸 `Authorization: Bearer …` 值,防止令牌泄漏到捕获的流量中。([#4358](https://github.com/diegosouzapw/OmniRoute/pull/4358)) - **fix(pricing): 为 `gpt-5.x-pro` OpenAI 模型定价 + 对齐 opencode-go 发现测试** — 为 gpt-5.x-pro 模型添加定价,使成本遥测报告真实成本而非零。([#4355](https://github.com/diegosouzapw/OmniRoute/pull/4355)) - **fix(sse): 在中止/错误时释放读取器并取消流(不再有 Undici 池 Socket 泄漏)** — 在中止或流中错误时,响应读取器被释放且流被取消,防止泄漏的池化 Socket 降低后续请求的性能。([#4309](https://github.com/diegosouzapw/OmniRoute/pull/4309) — 感谢 @Ardem2025) - **fix(kiro): 发送早期的仅角色起始块以释放流就绪门** — Kiro 流现在发送初始的仅角色块,使流就绪门及时释放而非停滞。([#4311](https://github.com/diegosouzapw/OmniRoute/pull/4311) — 感谢 @artickc) - **fix(dashboard): 代理模态框停止将新范围预填充为无关代理** — 添加新范围分配不再继承之前选中的代理的配置。([#4312](https://github.com/diegosouzapw/OmniRoute/pull/4312)) - **fix(open-sse): inner-ai 停止将未匹配模型静默重路由到 `models[0]`** — 未匹配的模型 ID 不再由第一个可用模型静默提供服务;查找现在返回 null,请求被显式处理。([#4310](https://github.com/diegosouzapw/OmniRoute/pull/4310)) - **fix(pollinations): 处理需要认证的高级模型(claude、gemini、midjourney)** — 需要认证的高级 Pollinations 模型现在正确处理而非失败。([#4266](https://github.com/diegosouzapw/OmniRoute/pull/4266) — 感谢 @oyi77) - **fix(codex): 隔离 Spark 配额范围** — Codex Spark 使用量现在在自己的配额范围内跟踪,不再混入其他 Codex 配额。([#4293](https://github.com/diegosouzapw/OmniRoute/pull/4293) — 感谢 @xz-dev) - **fix(dashboard): 改进 API"试用"功能** — 修复控制台 API"试用"面板使用的请求路径。([#4296](https://github.com/diegosouzapw/OmniRoute/pull/4296) — 感谢 @edrickrenan) - **fix: 为非安全上下文 polyfill `crypto.randomUUID`** — 当控制台通过非安全(纯 HTTP)来源提供服务且 `crypto.randomUUID` 不可用时,恢复 UUID 生成。([#4287](https://github.com/diegosouzapw/OmniRoute/pull/4287) — 感谢 @pizzav-xyz) - **fix(proxy): 允许并发的代理调度器流** — 代理调度器不再序列化流,使通过代理连接的并发请求可以并行运行。([#4288](https://github.com/diegosouzapw/OmniRoute/pull/4288) — 感谢 @wilsonicdev) - **fix(build): 将 llmlingua SLM 可选包共置到 `dist/node_modules`(postinstall)** — 可选的 llmlingua SLM 包被共置到独立构建中,使压缩 worker 可以在生产环境中实际启动。([#4286](https://github.com/diegosouzapw/OmniRoute/pull/4286)) - **fix(mitm): 在流量检查器中展示 AgentBridge 流量(D4 摄取)** — AgentBridge 请求现在出现在流量检查器中。([#4285](https://github.com/diegosouzapw/OmniRoute/pull/4285)) - **fix(sse): 在调度器失败时展示 undici `err.cause`** — 调度器失败现在将原因链(和 `AggregateError`)展平到错误详情中以供诊断。([#4281](https://github.com/diegosouzapw/OmniRoute/pull/4281)) - **fix(cli): 使用 free-claude-code 模式加固 `launch`/`launch-codex`** — 启动器采用了从 free-claude-code 移植的加固启动模式。([#4278](https://github.com/diegosouzapw/OmniRoute/pull/4278)) - **fix(compression): 端到端审计 — 修复整个压缩流程** — 对压缩管线进行全面扫描,修复 ultra/aggressive/lossless 边缘情况、无障碍锚点处理、语言检测和模式解耦。([#4323](https://github.com/diegosouzapw/OmniRoute/pull/4323)) ### 🧪 测试 - **test: 对齐被合并 PR 遗留的两个红色测试** — 在并发合并后重新对齐 db-rules 分类计数(#4335)和 LMArena 分片 Cookie 元数据测试(#4271)。([#4346](https://github.com/diegosouzapw/OmniRoute/pull/4346)) - **test(ci): 协调 release/v3.8.30 基线 + 测试漂移** — 协调发布分支上累积的质量基线和漂移测试。([#4276](https://github.com/diegosouzapw/OmniRoute/pull/4276)) ### 📝 维护 - **refactor(combo): `ComboContext` + 提取 `phaseComboSetup`(巨文件拆分,阶段 1)** — 开始分解 Combo 巨文件,将 Combo 设置提取到上下文对象中,不触及调度/信号量逻辑。([#4326](https://github.com/diegosouzapw/OmniRoute/pull/4326)) - **feat(quality): 限制测试文件大小 — 反膨胀第 1 层** — 冻结现有巨测试并将新测试文件限制在 800 行以内以阻止重新膨胀。([#4273](https://github.com/diegosouzapw/OmniRoute/pull/4273)) - **feat(quality): 设定每个模块的 mutationScore 下限 + 阻塞性聚合棘轮(T3)** — 添加每个模块的变异分数下限和阻塞性聚合门禁。([#4305](https://github.com/diegosouzapw/OmniRoute/pull/4305)) - **feat(quality): 使 a11y 门禁真正生效(`@axe-core/playwright` 加入夜间运行)** — 将之前虚设的无障碍门禁接入夜间运行并使用真实基线。([#4321](https://github.com/diegosouzapw/OmniRoute/pull/4321)) - **feat(quality): 解除 R1 阻塞 — 通过 `disableBail` 进行测试冗余度测量** — 启用之前被 fail-fast 阻塞的测试冗余度测量。([#4322](https://github.com/diegosouzapw/OmniRoute/pull/4322)) - **fix(quality): 复杂度门禁现在覆盖 `bin/` + `electron/`,且 tracked-artifacts 在 pre-commit 中运行** — 扩展复杂度门禁的范围并将 tracked-artifacts 检查移入 pre-commit 钩子。([#4318](https://github.com/diegosouzapw/OmniRoute/pull/4318)) - **fix(quality): 恢复 release/v3.8.30 绿色 — 3 个来自并发合并的潜在红色** — 修复三个因并发合并到发布分支而暴露的潜在测试红色。([#4335](https://github.com/diegosouzapw/OmniRoute/pull/4335)) - **fix(combo): 保持 `phaseComboSetup` 在复杂度上限以下** — 提取辅助函数使新的 Combo 设置阶段保持在复杂度门禁以下。([#4338](https://github.com/diegosouzapw/OmniRoute/pull/4338)) - **ci(mutation): 按范围/配对拆分超预算批次,使每个批次符合作业上限** — 重新拆分变异批次使每个批次符合 CI 作业预算。([#4272](https://github.com/diegosouzapw/OmniRoute/pull/4272)) - **chore(ci): 将 electron 审计门禁对齐到根 advisory 策略** — electron 工作区审计门禁现在遵循与根相同的 advisory 策略。([#4275](https://github.com/diegosouzapw/OmniRoute/pull/4275)) - **chore(quality): 协调并发合并漂移下的复杂度/质量基线** — 汇总该周期由并发合并驱动的基线协调,合并到发布分支。([#4330](https://github.com/diegosouzapw/OmniRoute/pull/4330), [#4336](https://github.com/diegosouzapw/OmniRoute/pull/4336), [#4370](https://github.com/diegosouzapw/OmniRoute/pull/4370)) - **docs: 禁止在提交/PR/CHANGELOG 中使用 AI 生成页脚(硬规则 #16)** — 将禁止 AI 生成页脚和机器人共同作者尾注的规定编入规则。([#4328](https://github.com/diegosouzapw/OmniRoute/pull/4328)) - **docs(design): 添加 OmniRoute 设计系统和视觉标识规范** — 添加设计系统 / 视觉标识规范文档。(感谢 @diegosouzapw) ### 🔒 安全 - **fix(sse): 加固 DuckDuckGo lite 抓取器清理逻辑(CodeQL)** — 关闭无密钥网页搜索抓取器中的四个 HIGH 级别 CodeQL 告警:`decodeEntities` 现在**最后**解析 `&`,使已转义的实体(例如 `&lt;`)作为字面文本保留而非被双重反转义(`js/double-escaping`);`stripTags` 先解码实体,然后循环剥离标签直至不动点并丢弃任何尾部未闭合的 `<…`,使实体编码的标记如 `<script>` 永远无法以活动标签形式到达 LLM/客户端(`js/incomplete-multi-character-sanitization`);搜索测试中的主机检查使用 `new URL().hostname` 相等性而非子字符串 `.includes`(`js/incomplete-url-substring-sanitization`)。([#4356](https://github.com/diegosouzapw/OmniRoute/pull/4356)) ### 🔧 依赖 - **fix(deps): 将 undici 升级到 7.28.0,dompurify 升级到 3.4.11(安全)** — 解决 undici SOCKS5-TLS / 缓存安全公告和 dompurify 安全公告。([#4306](https://github.com/diegosouzapw/OmniRoute/pull/4306)) - **chore(deps): 将 actions/checkout 从 4 升级到 7** — CI checkout-action 更新。([#4297](https://github.com/diegosouzapw/OmniRoute/pull/4297)) - **fix(executors): 为 qwen 非流式 / 思考模式 Claude Code 请求剥离 `stream_options`** — Claude-Code 兼容服务商通过 `upstreamStream = stream || isClaudeCodeCompatible`(`open-sse/handlers/chatCore.ts`)强制开启执行器级别的 `stream` 标志,但传出请求体保留调用者原始的 `stream: false`。`DefaultExecutor.transformRequest` 中共享的 `stream && targetFormat === "openai"` 分支随后将 `stream_options: { include_usage: true }` 注入到一个仍然声明 `stream: false` 的请求体上,qwen 上游以 `400 "'stream_options' only set this when you set stream: true"` 拒绝。当请求体携带 `thinking` / `enable_thinking` 时出现相同的拒绝。qwen 分支现在在请求体显式声明 `stream: false` 或请求思考模式时跳过注入(并剥离任何客户端发送的 `stream_options`),使常规 qwen 流式请求的 usage 注入保持不变。(感谢 @anuragg-saxenaa) --- ## [3.8.29] — 2026-06-19 ### ✨ 新功能 - **feat(cloud-agent): 通过官方 API 密钥 REST API 使用 Cursor Cloud Agent(无 IDE-OAuth 封禁风险)** — 新增 `cursor-cloud` 云代理,通过官方 REST API(`api.cursor.com`)驱动 Cursor 的后台/云代理,使用用户或服务账户 API 密钥进行认证 — 这是比复用 Cursor IDE 的 OAuth 会话(即现有的 `cursor` 服务商,带有封禁风险警告)更安全的第一方替代方案。实现为纯 REST 适配器,镜像 Devin/Jules 代理(`createTask`/`getStatus`/`sendMessage`/`listSources`),因此**不会**引入 `@cursor/sdk` 包及其各平台原生二进制文件(Cursor 的 SDK 本身只是此 REST API 的一个薄封装)。Cursor 的大写状态枚举(`CREATING`/`RUNNING`/`FINISHED`/`ERROR`)被显式映射到共享的 `CloudAgentStatus`,并且 `baseUrl` 可按凭证覆盖。凭证通过现有的 `cloud_agent_credentials` 表加密存储;无需 Schema 变更。([#4227](https://github.com/diegosouzapw/OmniRoute/issues/4227) — 感谢 @MRDGH2821) - **feat(routing): OpenRouter 风格 `auto/:` 组合** — 自动路由现在支持带后缀的组合,将 _类别_(路由类型)与 _层级_(优化方式)分离:`auto/coding:fast`、`auto/coding:cheap`(别名 `:floor`)、`auto/coding:free`、`auto/coding:pro`、`auto/coding:reliable`,以及新的类别根 `auto/reasoning`、`auto/vision`、`auto/multimodal`。**层级**选择评分权重 — `:fast` → 快速发送,`:cheap`/`:floor` → 成本节省,`:reliable` → 新的可靠性优先方案(熔断器健康度 + 延迟稳定性)— 而 `:free`/`:pro` 按模型层级过滤候选池(`classifyTier`:免费层 vs. 高级模型)。**类别**按能力过滤候选池(`vision`/`multimodal` → 支持视觉的模型,`reasoning` → 推理/思考模型)。任何有效的 `auto/:` 均可按需解析;精选集合在 `/v1/模型` 和控制台中展示。过滤采用故障开放策略 — 如果约束条件未匹配到任何已连接的模型,则使用完整候选池,确保路由永不中断。所有组合逻辑位于新的 `open-sse/services/autoCombo/suffixComposition.ts` 中;核心 Combo 评分器(`Combo.ts`)保持不变。#4235 的第二部分(高级账户层级权重为后续跟进)。([#4235](https://github.com/diegosouzapw/OmniRoute/issues/4235) — 感谢 @MRDGH2821) - **feat(routing): 展示 `auto/cheap`、`auto/offline`、`auto/smart` 组合(目录 ↔ README 同步)** — README 中列出了 `auto/cheap`(每令牌最便宜优先)、`auto/offline`(配额/速率限制余量最多优先)和 `auto/smart`(质量优先 + 10% 探索),它们在请求时已通过 `parseAutoPrefix` → `createVirtualAutoCombo` 正常解析。但它们之前缺失于 `AUTO_TEMPLATE_VARIANTS`,因此 `/v1/模型` 和控制台组合列表(遍历该目录)从未显示它们 — 目录与文档产生偏差(可在 issue 截图中看到)。新增这三个条目,使其与其它内置 `auto/*` 组合一起在所有位置展示。#4235 的第一部分(OpenRouter 风格 `auto/:` 后缀 + 新类别随后跟进)。([#4235](https://github.com/diegosouzapw/OmniRoute/issues/4235) — 感谢 @MRDGH2821) - **feat(cli): 远程模式 — 使用作用域访问令牌驱动远程 OmniRoute** — 新增 CLI 模式,使用作用域访问令牌连接到远程 OmniRoute 实例,使本地 CLI 可以驱动你没有会话的服务器。([#4256](https://github.com/diegosouzapw/OmniRoute/pull/4256)) - **feat(api): 成本遥测对齐 — 每个端点均输出 `X-OmniRoute-*` 请求头 + 非令牌成本引擎** — 每个端点现在均输出 `X-OmniRoute-*` 成本/用量请求头,由同样对非令牌(媒体/基于请求的)用量进行计价的成本引擎提供支持。([#4247](https://github.com/diegosouzapw/OmniRoute/pull/4247)) - **feat(api): 注册 Kimi K2.7 Code 模型(`kimi-k2.7-code` + `-highspeed`)** — 新的 Moonshot 纯思考编码模型已注册(固定采样;`temperature`/`top_p` 标记为不支持)。([#4183](https://github.com/diegosouzapw/OmniRoute/pull/4183)) - **feat(catalog): 将 `kimi-k2.7-code` 添加到 kmca 目录 + qwen-web 模型发现** — 在 kmca 目录中展示新的 Kimi 编码模型,并将 qwen-web 接入模型发现。([#4185](https://github.com/diegosouzapw/OmniRoute/pull/4185)) - **feat(api): 扩展 `zai` 服务商目录,新增 GLM-5.2 / GLM-4.7** — 将真实的 GLM-5.2、GLM-4.7 和 GLM-4.7-flash 模型 ID 添加到 Anthropic 直连的 `zai` 服务商。([#4201](https://github.com/diegosouzapw/OmniRoute/pull/4201)) - **feat(api): 无思考网关模型 ID(FCC 移植,Fase 8.1)** — 强制关闭思考的网关模型 ID 变体,从 free-claude-code 移植。([#4145](https://github.com/diegosouzapw/OmniRoute/pull/4145)) - **feat(sse): 截断流的中流续传(FCC 移植,Task 4.4)** — 当流被截断时,OmniRoute 可以透明地继续它,从 free-claude-code 移植。([#4147](https://github.com/diegosouzapw/OmniRoute/pull/4147)) - **feat(sse): 每服务商滑动窗口速率限制容灾(FCC 移植,Fase 8.2)** — 每服务商滑动窗口速率限制器作为容灾路径,从 free-claude-code 移植。([#4146](https://github.com/diegosouzapw/OmniRoute/pull/4146)) - **feat(sse): 透明流恢复(FCC 移植,Fase 4,可选启用)** — 可选启用的中断上游流透明恢复,从 free-claude-code 移植。([#4131](https://github.com/diegosouzapw/OmniRoute/pull/4131)) - **feat(search): 免费 DuckDuckGo 网页搜索作为最后手段服务商(FCC 移植,Fase 6)** — 新增无需密钥的 DuckDuckGo 网页搜索服务商,作为最后手段使用,从 free-claude-code 移植。([#4136](https://github.com/diegosouzapw/OmniRoute/pull/4136)) - **feat(logging): pino 日志器中的凭证脱敏安全网(FCC 移植,Fase 8.3)** — 日志器级别的脱敏处理,从日志输出中清除凭证,从 free-claude-code 移植。([#4140](https://github.com/diegosouzapw/OmniRoute/pull/4140)) - **feat(memory): 可选启用 Qdrant 标量 int8 量化(F4.4 Q1)** — 对 Qdrant 后端记忆向量进行可选启用的 int8 标量量化。([#4187](https://github.com/diegosouzapw/OmniRoute/pull/4187)) - **feat(memory): 可选启用 sqlite-vec int8 向量量化(F4.4 Q2)** — 对 sqlite-vec 记忆后端进行可选启用的 int8 量化。([#4190](https://github.com/diegosouzapw/OmniRoute/pull/4190)) - **feat(deploy): `update` 时保留可选依赖(`--include=optional`)** — 原地更新路径现在传递 `--include=optional`,使原生/可选包在更新时不会被丢弃。([#4260](https://github.com/diegosouzapw/OmniRoute/pull/4260)) - **feat(dashboard): 统一视觉标识 — 网格、基础元素、表格、表单控件(设计阶段 1-4)** — 一次全面的设计调整,使控制台与主站对齐:网格壁纸、按钮/卡片/输入基础元素、主题感知表格和表单控件。([#4122](https://github.com/diegosouzapw/OmniRoute/pull/4122)) - **feat(dashboard): 所有独立屏幕的网格壁纸 + 流畅 4K 布局** — 标识网格现在覆盖所有独立屏幕,布局可流畅缩放到 4K。([#4158](https://github.com/diegosouzapw/OmniRoute/pull/4158)) - **feat(dashboard): 使标识网格可见 + 统一强调色焦点环** — 设计跟进,使网格实际可见并将焦点环标准化为强调色。([#4141](https://github.com/diegosouzapw/OmniRoute/pull/4141)) - **feat(dashboard): 仅导入免费模型 + 免费模型列表控件** — 模型导入页面可以仅导入免费模型,并提供管理免费模型列表的控件。([#4176](https://github.com/diegosouzapw/OmniRoute/pull/4176) — 感谢 @felipesartori) - **feat(dashboard): 无认证服务商账户的紧凑网格布局** — 认证禁用时,服务商账户使用更密集的网格布局。([#4137](https://github.com/diegosouzapw/OmniRoute/pull/4137) — 感谢 @felipesartori) - **feat(dashboard): 从注册表派生媒体 `serviceKinds`(展示 MiniMax + 媒体目录)** — `/media-providers/[kind]` 现在从注册表派生其服务类型,而非手动维护的列表,展示约 48 个之前不可见的媒体服务商(包括 MiniMax TTS/视频/音乐)。([#4212](https://github.com/diegosouzapw/OmniRoute/pull/4212)) - **feat(traffic-inspector): 实时(进行中)请求过滤(Gap 5)** — 流量检查器现在可以过滤进行中的请求。([#4130](https://github.com/diegosouzapw/OmniRoute/pull/4130)) - **feat(agent-bridge): 维护与诊断仪表盘控件** — 为 Agent Bridge 向控制台添加维护和诊断控件。([#4127](https://github.com/diegosouzapw/OmniRoute/pull/4127)) - **feat(mitm): TPROXY IP_TRANSPARENT 原生插件 + 条件加载器(Epic A)** — 原生 `IP_TRANSPARENT` 插件及条件加载器,为 TPROXY 捕获奠定基础。([#4148](https://github.com/diegosouzapw/OmniRoute/pull/4148)) - **feat(mitm): Fase 3 Epic A 探索 — TPROXY 命令构建器** — iptables/TPROXY 命令集的事务式构建器。([#4139](https://github.com/diegosouzapw/OmniRoute/pull/4139)) - **feat(mitm): TPROXY 设置层 — 事务式应用/回滚(Epic A)** — 以事务方式应用和回滚 TPROXY 路由设置。([#4144](https://github.com/diegosouzapw/OmniRoute/pull/4144)) - **feat(mitm): 向 TPROXY 插件添加 `setSocketMark`(反循环原语)** — 暴露 `setSocketMark`,使 OmniRoute 自身的出口流量可以被标记并跳过(反循环)。([#4160](https://github.com/diegosouzapw/OmniRoute/pull/4160)) - **feat(mitm): TPROXY 捕获模式监听器 + `connectMarked`(Epic A)** — 捕获模式监听器加上标记连接原语。([#4169](https://github.com/diegosouzapw/OmniRoute/pull/4169)) - **feat(mitm): 针对 TPROXY 的动态每 SNI 证书颁发机构(TLS 解密 1/N)** — 每 SNI 即时证书颁发机构,TLS 解密的第一部分。([#4173](https://github.com/diegosouzapw/OmniRoute/pull/4173)) - **feat(mitm): 针对 TPROXY 的 TLS 终结捕获(解密 2/N)** — 终结 TLS 以捕获解密流量。([#4179](https://github.com/diegosouzapw/OmniRoute/pull/4179)) - **feat(mitm): 将 TLS 解密引擎接入 TPROXY 捕获模式(解密 3/N)** — 将解密引擎连接到捕获模式管线。([#4200](https://github.com/diegosouzapw/OmniRoute/pull/4200)) - **feat(mitm): TPROXY 捕获模式管理器(解密 4a/N)** — 协调 TPROXY 捕获生命周期的管理器。([#4208](https://github.com/diegosouzapw/OmniRoute/pull/4208)) - **feat(mitm): 本地专用路由 + 信任存储安装器,用于 TPROXY 解密(4b/N)** — 仅回环的管理路由加上解密 CA 的 CA 信任存储安装器。([#4211](https://github.com/diegosouzapw/OmniRoute/pull/4211)) - **feat(dashboard): 流量检查器中的 TPROXY 解密捕获开关(4c/N)** — 用于启用/禁用解密捕获的 UI 开关。([#4216](https://github.com/diegosouzapw/OmniRoute/pull/4216)) - **feat(compression): 用内置 GCF 替换余量表格编码器** — 将表格编码器替换为内置 GCF 实现。([#4167](https://github.com/diegosouzapw/OmniRoute/pull/4167) — 感谢 @blackwell-systems) - **feat(compression): 通过 `compression.step` 实现每引擎实时流式传输(F3.3)** — 通过 `压缩.step` 事件流式传输每引擎压缩进度。([#4217](https://github.com/diegosouzapw/OmniRoute/pull/4217)) - **feat(compression): 在 Studio 中为单引擎运行显示引擎节点** — 压缩 Studio 现在即使只有一个引擎运行时也会渲染引擎节点。([#4210](https://github.com/diegosouzapw/OmniRoute/pull/4210)) - **feat(compression): 通过 Canvas/Waterfall 开关暴露 WaterfallInspector** — 新增 Canvas/Waterfall 视图开关,展示 WaterfallInspector。([#4238](https://github.com/diegosouzapw/OmniRoute/pull/4238)) - **feat(compression): 通过设置子路由使 `mcpAccessibility` 配置可达** — 在专用设置子路由下暴露 `mcpAccessibility` 配置。([#4237](https://github.com/diegosouzapw/OmniRoute/pull/4237)) - **feat(compression): 可运行的 A/B 基准测试 CLI(F2.4)** — 用于运行 A/B 压缩基准测试的 CLI。([#4220](https://github.com/diegosouzapw/OmniRoute/pull/4220)) - **feat(compression): 向回放测试工具添加转录加载器** — 回放测试工具现在可以加载真实转录。([#4246](https://github.com/diegosouzapw/OmniRoute/pull/4246)) - **feat(compression): 接入 MCP 工具基数削减(F4.3,可选启用)** — 可选启用的 MCP 工具集基数削减,以缩小提示。([#4221](https://github.com/diegosouzapw/OmniRoute/pull/4221)) - **feat(compression): 接入 RTK 注释剥离配置 + 遵循 `preserveDocstrings`** — RTK 注释剥离现在可配置并遵循 `preserveDocstrings` 标志。([#4242](https://github.com/diegosouzapw/OmniRoute/pull/4242)) - **feat(compression): 遵循每过滤器 RTK `deduplicate` 标志** — RTK 过滤器现在遵循每过滤器的 `deduplicate` 标志。([#4231](https://github.com/diegosouzapw/OmniRoute/pull/4231)) - **feat(compression): 在堆叠循环中遵循注册表 `enabled` 标志** — 堆叠压缩循环现在跳过注册表中已禁用的引擎。([#4244](https://github.com/diegosouzapw/OmniRoute/pull/4244)) - **feat(compression): 持久化 RTK 分组配置(解锁 R5 `enableGrouping`)** — 持久化 RTK 分组配置,解锁 R5 `enableGrouping` 规则。([#4207](https://github.com/diegosouzapw/OmniRoute/pull/4207)) - **feat(compression): 将 ultra 的 `modelPath`/`slmFallbackToAggressive` 接入 LLMLingua SLM 层** — 将 ultra 层的小语言模型旋钮连接到 LLMLingua SLM 路径。([#4257](https://github.com/diegosouzapw/OmniRoute/pull/4257)) - **feat(quality): Onda 2 变异门工具 — 射线分类器(T1)+ `mutationScore` 棘轮(T3)** — 新的变异测试工具:幸存者射线分类器和 `mutationScore` 棘轮。([#4234](https://github.com/diegosouzapw/OmniRoute/pull/4234)) - **feat(ci): 接入 F2.4 压缩预算门棘轮** — 新增 CI 棘轮,防止压缩预算回退。([#4232](https://github.com/diegosouzapw/OmniRoute/pull/4232)) ### 🐛 问题修复 - **fix(providers): qwen-web 模型发现现在列出实时目录而非空目录** — `qwen-web` cookie 服务商在 `PROVIDER_MODELS_CONFIG` 中没有条目,因此其模型发现页面返回空/过时的本地目录(路由顶部的 OAuth 容灾仅对 `服务商 === "qwen"` 生效,导致 `qwen-web` 落入无配置分支)。新增一个 `qwen-web` 条目,获取**公开**的 `https://chat.qwen.ai/api/v2/模型` 端点(无需认证请求头)并解析 `{ data: { data: [{ id, name, owned_by }] } }` 结构(带更扁平的 `{ data: [] }` 容灾)。这是 #3931 的问题 #3(由 @thezukiru 诊断);问题 #1 — 校验器裸令牌误报 — 已在 #3958 中发布,问题 #2 — Qwen WAF 在流式传输端点的机器人检测导致空流 — 仍然是独立的上游/隐身问题。([#3931](https://github.com/diegosouzapw/OmniRoute/issues/3931) — 感谢 @thezukiru) - **fix(providers): ZenMux 模型发现现在列出实时目录(含免费模型)而非过时的 9 条目硬编码列表** — 添加 ZenMux 密钥校验正常,但连接随后显示 `API unavailable — using local catalog` 并缺少 ZenMux 宣传的免费模型(`z-ai/glm-5.2-free`、`moonshotai/kimi-k2.7-code-free`)。根本原因:`zenmux` 在注册表中带有正确的 `modelsUrl`,但是 — 与 #3976 之前的 `llm7`/`byteplus` 一样 — 它未被模型导入路由的任何实时获取分支分类(不是 `openai-compatible-*`、不是自托管、不在 `NAMED_OPENAI_STYLE_PROVIDERS` 中),因此路由从未探测上游 `/模型` 并落入注册表的硬编码 `模型[]`。将 `zenmux` 添加到 `NAMED_OPENAI_STYLE_PROVIDERS`,使路由探测 `https://zenmux.ai/api/v1/模型`(去掉 `/chat/completions` 的 `/模型` 候选)并提供实时列表,仅在上游获取失败时回退到本地目录 — 导入永不中断。([#4202](https://github.com/diegosouzapw/OmniRoute/issues/4202) — 感谢 @mikmaneggahommie) - **fix(providers): Vercel AI Gateway"导入模型"现在加载实时目录而非空目录** — 添加 Vercel AI Gateway 密钥有效,但在模型页面点击**导入**时没有加载任何可用内容(手动添加相同模型有效)。与 #4202(zenmux)/ #3976(llm7/byteplus)同类型:`vercel-ai-网关` 在注册表中带有真实的 `baseUrl`(`https://ai-网关.vercel.sh/v1/chat/completions`,格式 `openai`),但未被模型导入路由的任何实时获取分支分类(不是 `openai-compatible-*`、不是自托管、不在 `NAMED_OPENAI_STYLE_PROVIDERS` 中),因此路由从未探测上游 `/模型` 并落入注册表的 5 条目硬编码 `模型[]`。将 `vercel-ai-网关` 添加到 `NAMED_OPENAI_STYLE_PROVIDERS`,使路由探测 `https://ai-网关.vercel.sh/v1/模型`(去掉 `/chat/completions` 的 `/模型` 候选)并提供实时列表,仅在上游获取失败时回退到本地目录 — 导入永不中断。([#4249](https://github.com/diegosouzapw/OmniRoute/issues/4249) — 感谢 @FerLuisxd) - **fix(sse): 请求队列丢弃任务时提供清晰错误(不再有伪造上游的"This job timed out after Nms")** — 在并发负载下,超出每连接速率限制队列预算(`resilienceSettings.requestQueue.maxWaitMs`)的请求被 Bottleneck 以原始 `This job timed out after ms.` 消息丢弃。该字符串与上游网关超时无法区分,因此 502 响应体和调用日志 `last_error` 看起来像是跨无关服务商的服务商宕机(TI:0\|TO:0)— 一位运维人员花了约 3 小时将本地队列饱和误诊为上游故障。`withRateLimit` 现在将该特定 Bottleneck 错误重写为一条清晰的、OmniRoute 自有消息,指明可调参数(`requestQueue.maxWaitMs`,可在 Settings → Resilience 中调整),明确否认上游超时,保留原始错误作为 `cause`,并标记 `code: "RATE_LIMIT_QUEUE_TIMEOUT"`。行为不变 — 任务仍被丢弃,Combo 回退到下一个目标。([#4165](https://github.com/diegosouzapw/OmniRoute/issues/4165) — 感谢 @KooshaPari) - **fix(api): 在 `/v1/models` 中展示内置 `auto/*` 组合** — OmniRoute 附带零配置 `auto/*` 目录(`auto/best-coding`、`auto/pro-reasoning`、…、16 个变体),仪表盘已展示并可按需解析,但 `/v1/models` 列表仅输出持久化的 DB 组合 + 服务商模型。从 `/v1/models` 构建模型选择器的客户端(如 Hermes Agent)从未看到任何 `auto/*` 选项。目录现在在列表顶部输出每个 `AUTO_TEMPLATE_VARIANTS` ID(作为 `owned_by: "combo"`),并与持久化组合去重。(显示每个 `auto/*` 的动态选定成员是单独的增强功能。)([#4164](https://github.com/diegosouzapw/OmniRoute/issues/4164) — 感谢 @MRDGH2821) - **fix(sse): 在原生 Claude 路径上恢复 MCP / 第三方工具名称(Claude Code 中 MCP 调度中断)** — 自 3.8.27 起,通过 OmniRoute 路由到原生 Claude OAuth 服务商的每个 MCP 工具调用都在客户端失败,报错 `Error: No such tool available: `:工具 Schema 正常到达,但流式传输的 `tool_use.name` 以其伪装形式到达 Claude Code(例如 `McpN8nMcpSearchWorkflows` 而非注册的 `mcp__n8n-mcp__search_workflows`)。原生 Claude 工具名称伪装将其每请求别名→原始映射作为**不可枚举**的 `_toolNameMap` 存储在请求体上;3.8.27 中添加的请求检查器捕获从序列化形式(`JSON.parse(JSON.stringify(...))`)重建捕获体,这会丢弃不可枚举属性,因此 `finalBody._toolNameMap` 为空,响应端反伪装静默回退到静态内置映射 — 从不恢复动态 MCP / snake_case 名称。内置工具(Bash/Read/…)不受影响(静态映射);跨格式路径不受影响(它们以可枚举方式附加映射)。服务商请求捕获现在在捕获副本丢失时重新附加每请求映射(保持不可枚举,因此仍不会重新序列化到上游),恢复 MCP 工具调度。([#4091](https://github.com/diegosouzapw/OmniRoute/issues/4091) — 感谢 @pedrotecinf, @NakHalal) - **fix(dashboard): 日志自动刷新在嵌入/代理主机中自愈,这些主机会卡住或误触发可见性** — #4054 的后续:请求日志器在某些主机上仍然冻结自动刷新(3.8.28 Docker 上报,3.8.24 正常工作)。#4054 使初始可见性故障开放,但暂停是事件驱动的 — 主机触发一次性 `visibilitychange` → hidden 然后持续报告 `"hidden"`(或恢复但不再次触发事件),导致缓存的可见性标志卡在 `false`,因此间隔计时器在滴答但从不轮询(只有手动刷新按钮有效)。轮询滴答现在还会重新检查**实时** `document.visibilityState`,并且**窗口 `focus`** 监听器重新启用轮询(聚焦窗口是页面正在被查看的可靠信号)。真正处于后台的浏览器标签页仍然暂停(它报告 `"hidden"` 且从不接收焦点),保留 #3109 的网络饱和优化。([#4133](https://github.com/diegosouzapw/OmniRoute/issues/4133) — 感谢 @tjengbudi) - **fix(capabilities): 将视觉模型 ID 检测统一到一个共享源** — 三个代码路径各自维护独立的、漂移的视觉模型列表,因此同一个模型 ID 可能得到最多三种不同的判定。两个具体缺陷:lite 压缩的门控缺失 pixtral / llava / qwen-vl / glm-4v / kimi-vl / mistral-medium-3,因此它**对这些真正的视觉模型剥离图像并使其致盲**(与 #4071 / #4012 同类型);而 `/v1/模型` 列表过于宽泛,将文本模型(`gemma`、裸 `kimi` 如 `kimi-k2`)标记为视觉模型。所有三个(`modelCapabilities` 路由容灾、`/v1/模型` 列表、lite 图像剥离门控)现在委托给单一保守源 `src/shared/constants/visionModels.ts`,该源还恢复了 `glm-4v` / `gemini-3` 覆盖率并保留 #3328 MiniMax M3 的例外处理。([#4072](https://github.com/diegosouzapw/OmniRoute/issues/4072) — 感谢 @diego-anselmo) - **fix(sse): 暴露中流 Gemini 错误,而非返回截断的 200** — 当上游 Gemini SSE 流发出部分内容后发出 JSON 错误对象(`{"错误":{"code":503,"message":"…high demand…","status":"UNAVAILABLE"}}`)而非 `candidates` 载荷时,OmniRoute 静默丢弃它:gemini→openai 翻译器的无候选分支仅处理 `promptFeedback`(内容过滤器),对其他情况返回 `null`,因此流直接结束,客户端收到 HTTP 200 及截断体和 `finish_reason: "stop"` — 掩盖故障并跳过 Combo 容灾。`geminiToOpenAIResponse` 现在检测 `错误` 对象(可选地包裹在 `响应` 中),将其记录为 `state.upstreamError`(保留真实状态 — 503/`UNAVAILABLE`,或 429 对应 `RESOURCE_EXHAUSTED`),并通过现有 `onFailure`/`buildErrorBody`/`controller.错误` 路径将流出错 — 与 openai-responses 翻译器已使用的机制相同。([#4177](https://github.com/diegosouzapw/OmniRoute/issues/4177) — 感谢 @hartmark) - **fix(capabilities): 为 Mistral `-latest` 别名解析 models.dev 同步的视觉元数据** — #4071 启发式背后的根本原因:`getResolvedModelCapabilities("mistral/pixtral-12b-latest").supportsVision` 解析为 `null`(视觉仅来自 #4071 模型 ID 启发式,`attachment` 仍为 `null`),尽管模型.dev 将该模型列为多模态。经实时模型.dev API 确认:它将 Pixtral 12B 归类在**短** ID `pixtral-12b` 下(`attachment: true`、`modalities.input: ["text","image"]`),而请求使用 Mistral API 别名 `pixtral-12b-latest`。同步查询尝试了精确/原始/静态规范 ID — 全部错过短形式 — 因此落入启发式。`getSyncedCapabilityForResolved` 现在添加了最后手段容灾,去掉尾部 `-latest` 重试,使同步元数据(`attachment` / 图像模态)对这些别名生效;其 `-latest` ID 以完整形式存储的模型(如 `pixtral-large-latest`)继续直接解析。注意:模型.dev 同步目前仅支持手动(Settings → 模型.dev),没有定时刷新,因此新实例在同步运行前仍依赖 #4071 启发式 — 定期刷新节奏留作后续跟进。([#4073](https://github.com/diegosouzapw/OmniRoute/issues/4073) — 感谢 @diego-anselmo) - **fix(sse): 将 Xiaomi MiMo 推理控制映射到其原生 `thinking:{type}` 形式** — MiMo(`api.xiaomimimo.com`)**仅**通过顶级 `thinking:{type:"enabled"|"disabled"}` 控制思维链,不理解 OpenAI 的 `reasoning_effort`/`reasoning`,而其请求校验器是严格的(`400 Param Incorrect`)。OmniRoute 的 OpenAI 路径将推理意图作为 `reasoning_effort` 携带,而 claude→openai 翻译器可能留下 Claude 形式的 `thinking:{type, budget_tokens}` — 因此客户端的开关选择被静默丢弃,`budget_tokens`/`reasoning_effort` 作为额外参数附带,校验器可能拒绝。新的 `open-sse/services/mimoThinking.ts::normalizeMimoThinking`(在 `chatCore` 中为 `provider==="xiaomi-mimo"` 接入)将任何 thinking 对象精简为仅 `{type}`(`disabled` 保留;`enabled`/`adaptive`/其他 → `enabled`)并丢弃 `reasoning_effort`/`reasoning`。它故意**不**从裸 `reasoning_effort` 合成 thinking — `mimo-v2-omni` 是非思考模型,因此这可能会将静默忽略的参数变成硬错误。([#4224](https://github.com/diegosouzapw/OmniRoute/pull/4224)) - **fix(capabilities): Xiaomi MiMo `*-pro` 聊天模型仅支持文本(无视觉)** — 根据 Xiaomi 文档,仅 `mimo-v2.5` 和 `mimo-v2-omni` 接受图像;`mimo-v2.5-pro`/`mimo-v2-pro` 仅支持文本,但 `modelSpecs` 将其标记为支持视觉,且模型.dev 也错误标记([hermes-agent#18884](https://github.com/NousResearch/hermes-agent/issues/18884))。由于 `resolveVisionCapability` 让同步的 `attachment:true` 优先,图像请求可能被路由到不支持视觉的模型(#4071 故障模式)。修正了规格**并**在 `resolveVisionCapability` 中添加了硬覆盖(在同步分支之前检查,锚定使 `mimo-v2.5-pro` 永不匹配多模态 `mimo-v2.5`),覆盖错误的同步 attachment。还注册了缺失的原生 `mimo-v2-pro` 聊天模型和缺失的 `mimo-v2-tts` 语音模型。([#4224](https://github.com/diegosouzapw/OmniRoute/pull/4224)) - **fix(sse): Claude Opus 4.7+/Fable 5 仅使用自适应思考(不再有手动预算 400)** — Opus 4.7 及更高版本(Opus 4.7/4.8、Fable 5)移除了手动扩展思考:`thinking.type:"enabled"` 或**任何** `thinking.budget_tokens` 现在返回 `400`("Any request that tries to set a fixed thinking budget gets a 400" — Anthropic 迁移指南)。推理仅支持自适应,由 `output_config.effort` 控制。OmniRoute 的 OpenAI→Claude 翻译器将 `reasoning_effort` 低/中/高映射到手动 `thinking:{type:"enabled", budget_tokens}`,因此这些请求在最常用的服务商上硬 400(发送旧格式的 Claude 原生透传客户端也是如此)。新的 `adaptiveThinkingOnly` 模型标志现在驱动两项修复:翻译器将**每个**级别的 `reasoning_effort` 映射到 `{type:"adaptive"}` + `output_config.effort`(保留请求级别,永不设预算)用于这些模型,并且在现有翻译后思考规范化阻塞点的 `normalizeClaudeAdaptiveThinking` 兜底将任何残留的手动思考(透传旧格式、每模型默认值)折叠为 `{type:"adaptive"}`,以解析后的上游模型为键,覆盖所有路由模式。4.7 之前的模型(Opus 4.6/4.5、Sonnet、Haiku)保持手动预算不变。([#4230](https://github.com/diegosouzapw/OmniRoute/pull/4230)) - **fix(providers): 为 Claude Opus 4.7+/Fable 5 剥离非默认 temperature/top_p/top_k(固定采样 → 无 400)** — Opus 4.7 及更高版本以 `400` 拒绝非默认 `temperature`/`top_p`/`top_k`(采样固定;推理移至 `output_config.effort`)。翻译器无条件转发客户端提供的 `temperature`/`top_p`,且 Claude 注册表模型没有 `unsupportedParams`,因此对 `claude-opus-4-8` 的普通 OpenAI 格式请求 `temperature: 0.7` 硬 400。将 `unsupportedParams: ["temperature","top_p","top_k"]` 添加到 `claude`(短横线 `claude-opus-4-8`)和 `anthropic`(点号 `claude-opus-4.7`)注册表中的 Opus 4.7+/Fable 5 ID,使其在现有 `getUnsupportedParams` 调度阻塞点被剥离。4.7 之前的 Claude 模型仍接受采样参数。([#4230](https://github.com/diegosouzapw/OmniRoute/pull/4230)) - **fix(providers): 在 `openai` Chat Completions 路径上有条件剥离 GPT-5 推理的 temperature/top_p(推理激活时无 400)** — GPT-5 推理模型在推理激活时以 `400` 拒绝非默认 `temperature`/`top_p`,但在 `reasoning_effort:"none"`(GPT-5.1+ 默认值,即非推理模式)下又接受它们。在 `openai` 服务商上,仅 `o3` 带有 `REASONING_UNSUPPORTED`;`gpt-5.5`/`gpt-5.4`/`gpt-5.4-mini`/`gpt-5.4-nano` 没有采样守卫,因此 `temperature` + 激活推理请求硬 400。静态 `unsupportedParams` 列表无法表达 `none` 模式例外(会过度剥离合法情况),因此新的 `gpt5SamplingGuard` 仅在解析后的推理激活时丢弃 `temperature`/`top_p` — 接入现有 `getUnsupportedParams` 阻塞点,范围限定为 `openai` Chat Completions 接口(`codex` Responses 路径已由 CodexExecutor 允许列表覆盖;其他服务商不受影响)。([#4245](https://github.com/diegosouzapw/OmniRoute/pull/4245)) - **fix(codex): 停止静默丢弃 GPT-5 输出详细程度(`verbosity` / `text.verbosity`)** — GPT-5 系列新增输出详细程度控制:Chat Completions 上的 `verbosity`(low/medium/high),嵌套为 Responses API 上的 `text.verbosity`。CodexExecutor 通过允许列表过滤翻译后的请求,该列表没有 `text` 条目,因此对于 `codex` 服务商,提示在到达上游前被丢弃(`openai` Chat 路径已经转发它)。`normalizeCodexVerbosity` 现在在允许列表(现在允许 `text`)之前将到达的任何形式折叠为单个经过校验的 `text:{verbosity}`,并且 OpenAI Chat↔Responses 请求翻译器跨格式映射 `verbosity`,使提示在非 codex Responses 后端的格式转换中也能保留。无效/缺失的详细程度折叠为无 `text`(现状)。([#4245](https://github.com/diegosouzapw/OmniRoute/pull/4245)) - **fix(sse): 将 `reasoning_effort` 映射到 DeepSeek V4 的原生 `{high, max}` 词汇** — DeepSeek V4 仅理解 `high`/`max` 推理级别,因此其他 `reasoning_effort` 值被映射到其原生词汇而非被拒绝。([#4219](https://github.com/diegosouzapw/OmniRoute/pull/4219)) - **fix(glm): 为 GLM-5.2+ 思考设置默认 `max_tokens` 和延长超时** — GLM-5.2+ 思考响应较慢且需要余量,因此 OmniRoute 现在为其设置合理的默认 `max_tokens` 和更长的超时。([#4255](https://github.com/diegosouzapw/OmniRoute/pull/4255) — 感谢 @dhaern) - **fix(antigravity): 现代 Gemini 模型的默认 `includeThoughts`** — Antigravity 路径上的现代 Gemini 模型现在默认包含思考,使推理不会被静默丢弃。([#4180](https://github.com/diegosouzapw/OmniRoute/pull/4180) — 感谢 @dhaern) - **fix(provider-registry): 为 theoldllm 模型添加正确的 `contextLength`** — 为 theoldllm 的模型填入准确的上下文窗口大小。([#4184](https://github.com/diegosouzapw/OmniRoute/pull/4184) — 感谢 @herjarsa) - **fix(models): 暴露组合模型令牌限制** — `/v1/models` 现在报告组合模型的令牌限制。([#4189](https://github.com/diegosouzapw/OmniRoute/pull/4189) — 感谢 @megamen32) - **fix(combo): 保持透传配额容灾的范围限制** — 防止透传配额容灾泄漏到无关目标。([#4194](https://github.com/diegosouzapw/OmniRoute/pull/4194) — 感谢 @Svetznaniy33) - **fix(combo): 将主动容灾压缩纳入 TV1 逃生机制(无静默目标丢弃)** — 主动容灾压缩现在参与 TV1 逃生机制,确保目标永不静默丢弃。([#4228](https://github.com/diegosouzapw/OmniRoute/pull/4228)) - **fix(compression): 显示引擎预览输出** — 压缩 Studio 预览现在渲染引擎的输出。([#4128](https://github.com/diegosouzapw/OmniRoute/pull/4128) — 感谢 @megamen32) - **fix(compression): 加固引擎应对 I/O 故障和错误配置(F5.3)** — 压缩引擎在 I/O 错误和错误配置时优雅降级而非抛出异常。([#4198](https://github.com/diegosouzapw/OmniRoute/pull/4198)) - **fix(compression): 加固 RTK 原始输出脱敏 + 自定义过滤器的 ReDoS 守卫(F5.3)** — 扩大 RTK 原始输出脱敏范围并为用户提供的过滤器模式添加 ReDoS 守卫。([#4203](https://github.com/diegosouzapw/OmniRoute/pull/4203)) - **fix(compression): 在实时读取路径上限制 `mcpAccessibility` `maxTextChars`** — 实时读取路径现在钳制 `maxTextChars`,防止过小的值导致工具消失。([#4206](https://github.com/diegosouzapw/OmniRoute/pull/4206)) - **fix(dashboard): 数据表格绘制不透明表面,防止网格透出** — 数据表格现在在不透明表面上渲染,修复网格壁纸透出的问题。([#4233](https://github.com/diegosouzapw/OmniRoute/pull/4233)) - **fix(dashboard): 使服务商卡片悬停可见(之前约 1% 不透明度)** — 服务商卡片悬停状态之前几乎不可见;现在具有可见的表面。([#4214](https://github.com/diegosouzapw/OmniRoute/pull/4214)) - **fix(vscode): 清理隐式编辑器上下文** — 在发送到上游之前,从隐式 VS Code 编辑器上下文中脱敏敏感文件名/关键词。([#4124](https://github.com/diegosouzapw/OmniRoute/pull/4124) — 感谢 @zhiru) - **fix(build): 提高本地 `next build` 的 Node 堆内存以停止 OOM/卡顿** — 提高构建时堆内存,使本地生产构建不再 OOM 或卡顿。([#4171](https://github.com/diegosouzapw/OmniRoute/pull/4171)) - **fix(mitm): 基于 OUTPUT 链的 TPROXY 方案用于本地流量(VPS 端到端验证)** — 将 TPROXY 规则切换为 OUTPUT 链方案,使本地发起的流量被捕获;在 VPS 上端到端验证。([#4156](https://github.com/diegosouzapw/OmniRoute/pull/4156)) - **fix(mitm): 转发反循环 — 将绕过标记的套接字放在 Agent 上(解密 4d)** — 将绕过标记的套接字放在 HTTP Agent 上,使 OmniRoute 自身的转发流量永不重新进入捕获循环;VPS 验证。([#4229](https://github.com/diegosouzapw/OmniRoute/pull/4229)) - **fix(free-tiers): 退役失效的 `hasFree` 层级,标题四舍五入为 ~1.6B,重新生成每服务商表格** — 从标题计算中移除失效的免费层级并重新生成每服务商免费层级表格。([#4142](https://github.com/diegosouzapw/OmniRoute/pull/4142)) - **fix(free-tiers): 退役 4 个重新验证已失效的免费层级,标记 iflytek/sparkdesk ToS,澄清 monsterapi 一次性** — 移除四个确认已失效的免费层级并标注 ToS/一次性注意事项。([#4152](https://github.com/diegosouzapw/OmniRoute/pull/4152)) ### 🧪 测试 - **test(sse): 通过请求捕获往返守卫 Antigravity `_toolNameMap` 伪装映射** — #4091 的后续:`createPreparedRequestLogger().body()`(#4153)中的通用捕获修复重新附加了请求检查器在通过 `JSON.parse(JSON.stringify(...))` 重建上游体时丢弃的不可枚举 `_toolNameMap`,但唯一的回归测试仅覆盖了原生 Claude OAuth 伪装(PascalCase 别名)。Antigravity 伪装不同 — `cloakAntigravityToolPayload` 为自定义工具添加 `_ide` 后缀(`workspace_read` → `workspace_read_ide`),保持原生工具不变,并单独返回反向映射 — 因此 `providerRequestLogging.ts` 或执行器的重构可能在不触发 Claude 测试的情况下静默重新破坏 Antigravity 工具调度。新增专用回归测试,驱动真实的 `cloakAntigravityToolPayload` 通过捕获往返,断言 `_ide` 反向映射存活、保持不可枚举(永不重新序列化到上游),并且全原生流量不产生伪映射(验证在移除 #4153 重新附加时失败)。无生产变更。([#4181](https://github.com/diegosouzapw/OmniRoute/issues/4181) — 感谢 @hertznsk) - **test(chatcore): 6 个叶函数的专用单元测试 + 接入 stryker mutate(QG v2 Fase 9 T5 Fase 3)** — 为 6 个 chatCore 叶辅助函数添加聚焦单元测试并将其纳入变异测试。([#4218](https://github.com/diegosouzapw/OmniRoute/pull/4218)) - **test(chatcore): 遥测 / 记忆技能 / 语义缓存测试 + 将 2 个接入 stryker(QG v2 Fase 9 T5 Fase 3)** — 遥测、记忆技能和语义缓存叶函数的新测试,其中两个被添加到变异集。([#4222](https://github.com/diegosouzapw/OmniRoute/pull/4222)) - **test+ci(chatcore): semanticCache HIT 路径夹具(15/15 mutate)+ 350 分钟预算余量** — 将语义缓存 HIT 路径推进到完整的 15/15 变异分数,并为夜间认证/accountFallback 批次提供更多预算余量。([#4225](https://github.com/diegosouzapw/OmniRoute/pull/4225)) - **test(compression): 填补 F5.1 覆盖率缺口(回放 reducer、实时累加器、StatusDot)** — 填补剩余的 F5.1 压缩覆盖率缺口。([#4192](https://github.com/diegosouzapw/OmniRoute/pull/4192)) - **test(db,sse): 去抖动 db-backup + chatcore 流式传输时序断言** — 稳定两个时序敏感测试(即发即忘备份补全 + 流式传输竞态)。([#4132](https://github.com/diegosouzapw/OmniRoute/pull/4132)) - **test: 对齐 v3.8.28 之后在 main 上浮现的过时集成测试** — 重新对齐 v3.8.28 合并后漂移的集成测试。([#4129](https://github.com/diegosouzapw/OmniRoute/pull/4129)) ### 📝 维护 - **refactor(sse): 将 chatCore.ts 纯辅助函数拆分到 chatCore/ 模块(−561 LOC)** — 将纯辅助函数从 chatCore 上帝文件中提取到专用模块(Onda 3)。([#4159](https://github.com/diegosouzapw/OmniRoute/pull/4159)) - **refactor(chatcore): 提取透传/请求头/遥测辅助函数(QG v2 Fase 9 T5 C2-C3-C5)** — 进一步 chatCore 分解。([#4188](https://github.com/diegosouzapw/OmniRoute/pull/4188)) - **refactor(chatcore): 提取组合/代理上下文缓存 + 信号量辅助函数(QG v2 Fase 9 T5 C6-C7)** — 继续 chatCore 拆分。([#4193](https://github.com/diegosouzapw/OmniRoute/pull/4193)) - **refactor(combo): 上帝文件拆分试点 — 类型 + validateQuality + 谓词(QG v2 Fase 9 T5 D1-D3)** — Combo.ts 分解的第一部分。([#4162](https://github.com/diegosouzapw/OmniRoute/pull/4162)) - **refactor(combo): 上帝文件拆分第 2 部分 — shadow + 排序器 + 结构(QG v2 Fase 9 T5 D4-D6)** — 继续 Combo.ts 拆分。([#4175](https://github.com/diegosouzapw/OmniRoute/pull/4175)) - **refactor(combo): 上帝文件拆分第 3 部分 — auto 策略(QG v2 Fase 9 T5 D8)** — 从 Combo.ts 中提取 auto 策略。([#4186](https://github.com/diegosouzapw/OmniRoute/pull/4186)) - **refactor(combo): 将轮询粘性状态提取到 `combo/rrState.ts`(D7a)** — 将轮询粘性状态移到自己的模块中。([#4196](https://github.com/diegosouzapw/OmniRoute/pull/4196)) - **refactor(combo): 将重置感知配额块提取到 `combo/quotaStrategies.ts`(D7b)** — 将重置感知配额策略移到自己的模块中。([#4204](https://github.com/diegosouzapw/OmniRoute/pull/4204)) - **refactor(compression): 移除残留 SLM 接缝 + 已弃用的死别名** — 删除死压缩代码。([#4253](https://github.com/diegosouzapw/OmniRoute/pull/4253)) - **chore(compression): 移除残留的 reconstructCcr/SessionDedup 往返辅助函数** — 移除未使用的往返辅助函数。([#4226](https://github.com/diegosouzapw/OmniRoute/pull/4226)) - **chore(compression): 移除死导出 + 修复过时的 llmlingua 文档** — 修剪死导出并修正过时的 LLMLingua 文档。([#4223](https://github.com/diegosouzapw/OmniRoute/pull/4223)) - **chore(build): 在独立构建中构建并打包 TPROXY 原生插件(预构建 4e)** — 将原生 TPROXY 插件预构建包打包到独立构建中。([#4236](https://github.com/diegosouzapw/OmniRoute/pull/4236)) - **chore(ci): 将配额 + 6 个已覆盖的 chatCore 叶函数添加到 stryker mutate(QG v2 Fase 9 T5 Fase 3 后续)** — 将更多已覆盖叶函数纳入变异测试。([#4209](https://github.com/diegosouzapw/OmniRoute/pull/4209)) - **chore(ci): 将 8 个组合拆分叶函数重新添加到 stryker mutate + 扩展夜间批次矩阵 3→5(QG v2 Fase 9 T5 Fase 3)** — 恢复拆分 Combo 叶函数的变异覆盖率并扩展夜间矩阵。([#4205](https://github.com/diegosouzapw/OmniRoute/pull/4205)) - **chore(quality): 关闭 v3.8.28 周期门漂移(重新基线化 + 夜间变异范围)** — 调和 v3.8.28 周期后的质量门基线。([#4135](https://github.com/diegosouzapw/OmniRoute/pull/4135)) - **ci(mutation): 将夜间任务拆分为 3 个并行批次以适应 180 分钟预算(QG v2 Fase 9 T0)** — 并行化夜间变异运行。([#4150](https://github.com/diegosouzapw/OmniRoute/pull/4150)) - **ci(mutation): 恢复冷种子超时余量(a/b 在 #4225 合并中丢失)+ 扩展到 c/d/g/h** — 恢复并扩展每批次冷种子超时。([#4258](https://github.com/diegosouzapw/OmniRoute/pull/4258)) - **ci(docs): 加固虚构文档检查器 + 强制执行 `--strict`(QG v2 Fase 9 T9)** — 收紧反幻觉文档检查器。([#4149](https://github.com/diegosouzapw/OmniRoute/pull/4149)) - **ci: 从包版本派生 oasdiff base-ref + 标记变异工具链回归** — 修复 OpenAPI-diff base-ref 并暴露变异工具链回归。([#4134](https://github.com/diegosouzapw/OmniRoute/pull/4134)) - **docs(ci): 更正变异门注释(无回归 — `stryker -c` 是 `--concurrency`);记录 Task 12 GO** — 更正 stryker 标志的误读并记录探索 GO。([#4138](https://github.com/diegosouzapw/OmniRoute/pull/4138)) - **docs(api): 在 openapi.yaml 中记录 `/api/v1/ws` 聊天 WebSocket 端点** — 将 WebSocket 聊天端点添加到 OpenAPI 规范中。([#4215](https://github.com/diegosouzapw/OmniRoute/pull/4215)) - **docs(readme): 将 Acknowledgments 扩展为主题化、带星标的致谢大厅** — 重做 README 致谢部分。([#4195](https://github.com/diegosouzapw/OmniRoute/pull/4195)) - **style(dashboard): 将标识网格单元格缩小 46px → 32px(约小 30%)** — 收紧标识网格密度。([#4143](https://github.com/diegosouzapw/OmniRoute/pull/4143)) ### 🔧 依赖 - **deps: 将生产组更新 5 项** — 例行生产依赖更新。([#4121](https://github.com/diegosouzapw/OmniRoute/pull/4121)) - **chore(deps): 将 github/codeql-action 从 3 更新到 4** — CI action 更新。([#4120](https://github.com/diegosouzapw/OmniRoute/pull/4120)) - **chore(deps): 将 actions/setup-python 从 5 更新到 6** — CI action 更新。([#4119](https://github.com/diegosouzapw/OmniRoute/pull/4119)) --- ## [3.8.28] — 2026-06-17 ### ✨ 新功能 - **feat(providers): add OrcaRouter (OpenAI-compatible routing gateway)** — OrcaRouter 现已注册为 API-key 服务商。其自适应路由器以 `orcarouter/auto` 暴露(跨 150+ 上游模型的智能路由),以及精选旗舰模型系列(GPT-5.5、Gemini 3.5 Flash、Claude Opus 4.8、Grok 4.3、DeepSeek V4 Pro、MiniMax M2.7、Qwen3.7 Max)。已启用 `passthroughModels`,因此任何 OrcaRouter 模型 ID 均可使用。OpenAI 兼容端点(`https://api.orcarouter.ai/v1`),Bearer(`sk-orca-…`)认证 — 无需自定义 executor 或 translator。([#4070](https://github.com/diegosouzapw/OmniRoute/pull/4070) — 感谢 @jinhaosong-source) - **feat(providers): add Wafer AI (Anthropic-compatible, Bearer auth)** — Wafer AI 现已成为内置服务商,使用 Anthropic Messages 格式和 Bearer 认证,已注册其模型目录,开箱即用。([#4098](https://github.com/diegosouzapw/OmniRoute/pull/4098) — 感谢 @diegosouzapw) - **feat(cli): `omniroute launch` — 零配置 Claude Code launcher** — 新的 CLI 子命令,启动 OmniRoute(如果尚未运行)并启动预先配置好的 Claude Code,无需手动编辑 env/settings。([#4097](https://github.com/diegosouzapw/OmniRoute/pull/4097) — 感谢 @diegosouzapw) - **feat(api): exact offline token counting for the `count_tokens` fallback via tiktoken** — 本地 `count_tokens` 容灾现在使用真正的 tiktoken(BPE)分词器进行精确离线计数,而非启发式估算,因此即使上游计数端点不可达,token 预算也能保持准确。([#4087](https://github.com/diegosouzapw/OmniRoute/pull/4087) — 感谢 @diegosouzapw) - **feat(sse): Claude Code quota-probe bypass + command meta-request helpers** — 移植自 free-claude-code:OmniRoute 现在能识别 Claude Code 的配额探测和命令元请求,并在本地直接响应,而非消耗上游调用,从而减少 CLI 会话中的配额浪费。([#4083](https://github.com/diegosouzapw/OmniRoute/pull/4083) — 感谢 @diegosouzapw) - **feat(sse): generic 400 field-downgrade retry + Groq field stripping** — 当上游因不支持的字段而返回 `400` 时,OmniRoute 现在会去除该字段并重试(通用降级路径),同时内置了 Groq 专用字段去除逻辑。与现有的 `context_management` 重试处理对齐。([#4096](https://github.com/diegosouzapw/OmniRoute/pull/4096) — 感谢 @diegosouzapw) - **feat(sse): delegated Anthropic Context Editing — 中继覆盖率 + 400-容灾** — 扩展了 Claude 服务端 Context Editing 委托功能(#4021),提供更广泛的中继覆盖率和 `400`-容灾,使得上游因 context-management beta 而拒绝的请求能优雅降级而非直接失败。([#4065](https://github.com/diegosouzapw/OmniRoute/pull/4065) — 感谢 @diegosouzapw) - **feat(compression): record per-engine Context Editing telemetry** — 压缩管线现在记录 `context-editing` 引擎条目,使控制台能将服务端 Context Editing 的节省量与本地压缩引擎一并归因显示。([#4062](https://github.com/diegosouzapw/OmniRoute/pull/4062) — 感谢 @diegosouzapw) - **feat(compression): RTK learn/discover (sample source + API + UI)** — 基于规则的 RTK 压缩引擎新增 learn/discover 工作流:对源进行采样,通过新 API 展示候选规则,并在控制台中审查/应用它们。([#4088](https://github.com/diegosouzapw/OmniRoute/pull/4088) — 感谢 @diegosouzapw) - **feat(dashboard): 2026-06-17 free-tier refresh — honest catalog, uncapped + boost tiers, Layout A 预算表** — 免费套餐页面已刷新,包含真实、深度调研的目录(共享池/实际数据而非夸大的 24/7 RPM 估算),新增 `recurring-uncapped` 和 boost 套餐层级、新服务商,以及 KPI + 预算表(Layout A)。([#4089](https://github.com/diegosouzapw/OmniRoute/pull/4089) — 感谢 @diegosouzapw) - **feat(dashboard): Combo Studio connection-cooldown badge (U1b Slice 2)** — Combo Live 级联视图现在展示每个连接的冷却状态徽章,与 3.8.27 中发布的熔断器徽章互为补充。([#4068](https://github.com/diegosouzapw/OmniRoute/pull/4068) — 感谢 @diegosouzapw) - **feat(mitm): attribute intercepted requests to the originating process (Gap 1)** — 流量检查器现在能将每个被拦截的连接追溯到原始本地进程(通过 `/proc`),使得捕获的流量可以归因到产生它的应用。(ProxyBridge 风格强化。)([#4085](https://github.com/diegosouzapw/OmniRoute/pull/4085) — 感谢 @diegosouzapw) - **feat(mitm): capture-pipeline self-test route (Gap 12)** — 新增诊断路由,端到端地演练 MITM 捕获管线,使运维人员无需构造真实上游调用即可确认拦截功能正常。([#4093](https://github.com/diegosouzapw/OmniRoute/pull/4093) — 感谢 @diegosouzapw) - **feat(mitm): loop-guard self-check + verbosity control in `server.cjs` (Gaps 14+15)** — MITM 代理新增自引用循环防护(确保不会将自身流量代理到无限循环中),以及 `MITM_VERBOSE` 路由决策日志级别控制。([#4101](https://github.com/diegosouzapw/OmniRoute/pull/4101) — 感谢 @diegosouzapw) - **feat(agent-bridge): portable JSON import/export of config (Gap 4)** — Agent Bridge / MITM 配置现可导出并从便携 JSON 文件导入,方便备份工作设置或在机器间迁移。([#4094](https://github.com/diegosouzapw/OmniRoute/pull/4094) — 感谢 @diegosouzapw) ### 🐛 问题修复 - **fix(ws): start the LiveWS sidecar with `cwd` at the package root (global/systemd installs)** — 独立 LiveWS 启动器(`scripts/start-ws-server.mjs`)通过 `node --import tsx ` 重新启动自身,但未设置 `cwd`。当 WebSocket sidecar 从包目录外部启动时 — 例如全局 npm/homebrew 安装,或 `systemd`/`launchd` 单元从 `$HOME` 启动 — Node 无法解析 `tsx` 包(`ERR_MODULE_NOT_FOUND: Cannot find package 'tsx'`),即便在包目录下 `tsx` 也无法解析 tsconfig 的 `@/*` 路径别名(如 `@/types/databaseSettings`),导致 sidecar 无法启动。spawn 现在将 `cwd` 固定为包根目录(`scripts/` 的上级目录,`package.json` + `tsconfig.json` 所在位置),从而无论从何处启动,都能解析 `tsx` 和 `@/*` 别名。([#4055](https://github.com/diegosouzapw/OmniRoute/issues/4055) — 感谢 @Rahulsharma0810) - **fix(dashboard): Logs page auto-refresh now works in embedded/proxied dashboards** — 请求日志器曾将每次自动刷新 tick 依赖于静态的 `document.visibilityState === "visible"` 读取。那些报告永久非 `"visible"` 状态且从不触发 `visibilitychange` 事件的环境(Docker 控制台包装器、嵌入式 webview)会导致自动刷新完全冻结 — 只有手动刷新按钮有效,这是 3.8.24 无条件轮询的回归。暂停现在采用事件驱动和 fail-open 模式:轮询以启用状态启动,仅在真实的 `visibilitychange` → hidden 转换后才暂停(仍然保留对普通浏览器标签页的后台优化)。([#4054](https://github.com/diegosouzapw/OmniRoute/issues/4054) — 感谢 @tjengbudi) - **fix(docker): raise the build-stage Node heap to stop the production-build OOM** — Docker `builder` 阶段运行 `npm run build` 时使用 V8 的默认堆上限(~2 GB)。自 #4052 强制使用更重的 webpack 引擎后(Turbopack 在此 Next.js 版本上会 panic),生产优化阶段超过了该上限,构建在 `[builder] npm run build` 处以 `FATAL ERROR: … JavaScript heap out of memory` 失败。builder 阶段现在在构建前设置 `NODE_OPTIONS=--max-old-space-size`(默认 4096 MB,可通过 `--build-arg OMNIROUTE_BUILD_MEMORY_MB=…` 覆盖);该值会传递到启动的 `next build`。仅影响构建 — 运行时堆(runner 阶段的 `OMNIROUTE_MEMORY_MB`)保持不变。([#4076](https://github.com/diegosouzapw/OmniRoute/issues/4076) — 感谢 @kamenkadmitry) - **fix(dashboard): "Update Available" banner reappears reliably across Docker/npm/desktop installs** — 首页 banner 依赖于 `GET /api/system/version` 的 `updateAvailable`,该值仅通过 `npm` CLI 二进制文件执行 `npm info omniroute version --json` 来获取最新版本。当该二进制文件在运行时 PATH 中不存在(Docker/desktop/受限安装)或注册表不可达时,调用返回 `null` → `updateAvailable=false` → banner 静默地从不显示,即便已有更新的版本。路由现在通过 `resolveLatestVersion()` 解析最新版本:优先走快速的 `npm` CLI 路径,然后通过注册表 HTTP API(`registry.npmjs.org/omniroute/latest`)进行无 npm 二进制的容灾,两者都失败时记录警告而非静默降级。版本比较也经过强化,能容忍 `v` 前缀和预发布版本字符串。([#4100](https://github.com/diegosouzapw/OmniRoute/issues/4100)) - **fix(sse): route image requests only to confirmed-vision combo targets** — Combo 可能会将包含图像的请求路由到不支持视觉功能的成员。路由现在要求 `supportsVision === true`(加上模型 ID 启发式检查)后才向目标发送图像,确保多模态请求只落在能处理它们的成员上。([#4071](https://github.com/diegosouzapw/OmniRoute/pull/4071) — 感谢 @diego-anselmo) - **fix(security): injection guard respects the `INJECTION_GUARD_MODE` DB feature flag** — 提示注入防护曾忽略数据库功能标志,导致运维人员无法在运行时更改其模式;现在它会读取该标志并遵循配置的模式。([#4077](https://github.com/diegosouzapw/OmniRoute/pull/4077) — 感谢 @zhiru) - **fix(ws): proxy LAN `/live-ws` upgrades and warn on an unset `JWT_SECRET`** — 通过 LAN 代理路径到达的 WebSocket 升级请求未被转发到 LiveWS sidecar;现在已正确代理,且服务器在 `JWT_SECRET` 未设置时会记录明确的警告。([#4079](https://github.com/diegosouzapw/OmniRoute/pull/4079) — 感谢 @Rahulsharma0810) - **fix(dev): force webpack in the custom dev server (Turbopack 16.2.x panics)** — 自定义 dev server 现在强制使用 webpack 引擎,因为 Turbopack 在此 Next.js 版本上会 panic,从而确保 `npm run dev` 可靠启动。([#4092](https://github.com/diegosouzapw/OmniRoute/pull/4092) — 感谢 @chirag127) - **fix(auto): resolve built-in `auto/*` catalog combos** — 引用内置 `auto/*` Combo 时返回过早的 `400`,因为目录条目未被解析;内置 auto 目录现在在验证前完成解析,使这些 combo 正常工作。([#4058](https://github.com/diegosouzapw/OmniRoute/pull/4058) — 感谢 @megamen32) - **fix(sse): friendly 413 message for ChatGPT-web payload-too-large** — 过大的 ChatGPT-web 载荷曾返回模糊的错误;现在返回清晰的 `413` 和人类可读的消息。([#4080](https://github.com/diegosouzapw/OmniRoute/pull/4080) — 感谢 @diegosouzapw) - **fix(ws): warm the SSE auth import on LiveWS startup; relocate the boot test to integration** — the LiveWS sidecar now pre-imports the SSE 认证 module at startup to avoid a first-请求 stall, and its boot test was moved to the 集成 suite. ([#4063](https://github.com/diegosouzapw/OmniRoute/pull/4063) — 感谢 @diegosouzapw) - **fix(mitm): crash-safe system-state teardown + socket timeouts (ProxyBridge-inspired hardening)** — the MITM 代理 could leave the host's system 代理 settings applied if it crashed mid-teardown, and long-lived tunnels could leak as half-open sockets. Teardown 现在 crash-safe (system state is always restored) and proxied sockets get an idle timeout (`MITM_IDLE_TIMEOUT_MS`, 默认 60s). ([#4084](https://github.com/diegosouzapw/OmniRoute/pull/4084) — 感谢 @diegosouzapw) - **fix(responses): clear the `/v1/responses` keep-alive timer on cancel/abort** — 被取消或中止的 `/v1/responses` 流曾遗留 keep-alive 定时器运行,导致定时器泄漏和 CPU 浪费;定时器现在在 cancel/abort 时被清除。([#4105](https://github.com/diegosouzapw/OmniRoute/pull/4105) — 感谢 @artickc) - **fix(usage): reap orphaned pending-request details (unbounded memory leak)** — 请求永不完成的待处理请求详情条目无限累积;现在它们被回收,修复了缓慢的内存泄漏。([#4107](https://github.com/diegosouzapw/OmniRoute/pull/4107) — 感谢 @artickc) - **fix(auth): prune expired entries from the login brute-force guard map (unbounded growth)** — 登录暴力破解防护映射因过期条目从未被移除而无限制增长;过期条目现在被清理。([#4111](https://github.com/diegosouzapw/OmniRoute/pull/4111) — 感谢 @artickc) - **fix(logger): hard-cap the error-dedup map to bound memory under unique-message bursts** — 唯一错误消息的突发可能导致去重映射无限制增长;现在已硬性限制上限。([#4113](https://github.com/diegosouzapw/OmniRoute/pull/4113) — 感谢 @artickc) - **fix(circuit-breaker): enforce `MAX_REGISTRY_SIZE` (declared but never applied)** — 熔断器注册表声明了最大大小但从未强制执行,可能导致无限制增长;上限现在已生效。([#4114](https://github.com/diegosouzapw/OmniRoute/pull/4114) — 感谢 @artickc) - **fix(webhook): clear the abort timer in `finally` to avoid dangling timers on fetch error** — webhook 分发在清除 abort 定时器之前抛出异常时,定时器会悬空;现在在 `finally` 块中清除。([#4115](https://github.com/diegosouzapw/OmniRoute/pull/4115) — 感谢 @artickc) - **fix(combo): detach the per-target listener from the shared hedge abort signal** — Combo hedging 将每个目标的监听器附加到共享的 abort 信号上但未分离,导致跨请求的监听器泄漏;监听器现在被分离。([#4116](https://github.com/diegosouzapw/OmniRoute/pull/4116) — 感谢 @artickc) - **fix(timers): unref background interval timers so they don't block clean shutdown** — 长时间运行的后台间隔定时器保持事件循环活跃,阻止了干净的进程退出;它们现在被 `unref` 了。([#4117](https://github.com/diegosouzapw/OmniRoute/pull/4117) — 感谢 @artickc) ### ⚡ 性能优化 - **perf(registry): precompute the model→provider index in `parseModelFromRegistry`** — 模型→服务商查找现在使用预计算索引,而非每次调用时扫描注册表。([#4110](https://github.com/diegosouzapw/OmniRoute/pull/4110) — 感谢 @artickc) - **perf(obfuscation): cache per-word regexes instead of recompiling every request** — 混淆阶段现在缓存每个单词的正则表达式,而非每次请求时重新编译。([#4109](https://github.com/diegosouzapw/OmniRoute/pull/4109) — 感谢 @artickc) - **perf(stream): use `structuredClone` instead of JSON round-trip for per-chunk reasoning split** — 每个 chunk 的推理分割现在使用 `structuredClone` 而非 `JSON.parse(JSON.stringify(...))`。([#4108](https://github.com/diegosouzapw/OmniRoute/pull/4108) — 感谢 @artickc) - **perf(gemini): cache the reasoning close-tag regex instead of recompiling per token** — Gemini 推理关闭标签的正则表达式现在只编译一次并复用,而非每个 token 都重新编译。([#4106](https://github.com/diegosouzapw/OmniRoute/pull/4106) — 感谢 @artickc) ### 📝 维护 - **ci(quality): flip the TIA impacted-unit-tests gate from advisory to blocking (Fase 9)** — the test-impact-analysis gate that runs the unit tests impacted by a diff 现在 blocking on PRs. ([#4069](https://github.com/diegosouzapw/OmniRoute/pull/4069) — 感谢 @diegosouzapw) - **ci(quality): dedup the doubly-run `check:docs-sync` + record the validated ROI backlog (Fase 9)** — `check:docs-sync` was running twice in CI; the duplicate was removed and the validated quality-gate ROI backlog recorded. ([#4099](https://github.com/diegosouzapw/OmniRoute/pull/4099) — 感谢 @diegosouzapw) - **docs(quality-gates): reconcile the gate inventory with `ci.yml` + add the ROI rationalization backlog** — the quality-gate inventory doc was reconciled against the actual CI jobs and a rationalization backlog added. ([#4095](https://github.com/diegosouzapw/OmniRoute/pull/4095) — 感谢 @diegosouzapw) - **test(infra): isolate `DATA_DIR` per test process; raise Stryker concurrency 1→4** — test processes now get an isolated `DATA_DIR` (no shared-DB cross-talk) and the mutation runner's concurrency was raised. ([#4078](https://github.com/diegosouzapw/OmniRoute/pull/4078) — 感谢 @diegosouzapw) - **test(dashboard): smoke e2e for the Combo Live Studio page** — adds a Playwright smoke test covering the Combo Live Studio 页面. ([#4075](https://github.com/diegosouzapw/OmniRoute/pull/4075) — 感谢 @diegosouzapw) - **docs(compression): document LLMLingua optional deps + on-demand install** — documents the optional LLMLingua dependencies and how they are installed on demand. ([#4061](https://github.com/diegosouzapw/OmniRoute/pull/4061) — 感谢 @diegosouzapw) - **chore(deps): freeze `@huggingface/transformers` in dependabot (hard-pin)** — the transformers dependency is hard-pinned and frozen in dependabot to protect the VPS-validated LLMLingua + memory-embeddings stack from a breaking major bump. ([#4066](https://github.com/diegosouzapw/OmniRoute/pull/4066) — 感谢 @diegosouzapw) - **chore(docs): update the Discord invite link to a non-expiring one** — replaces the expiring Discord invite with a permanent link. ([#4067](https://github.com/diegosouzapw/OmniRoute/pull/4067) — 感谢 @diegosouzapw) - **chore(docs): document the new MITM env vars + reconcile the env-doc contract** — documents `MITM_IDLE_TIMEOUT_MS` and `MITM_VERBOSE` in `.env.example` + `ENVIRONMENT.md`, allowlists the framework-internal `TURBOPACK` and the Claude Code `ANTHROPIC_AUTH_TOKEN`, and relocates/prunes stale 服务商/guide docs. (thanks @diegosouzapw) ### 🔧 依赖 - **deps: bump the development group with 10 updates** — routine dependabot dev-dependency bumps. ([#4051](https://github.com/diegosouzapw/OmniRoute/pull/4051)) - **deps(electron): bump electron 42.4.0 → 42.4.1** — ([#4049](https://github.com/diegosouzapw/OmniRoute/pull/4049)) - **ci(deps): bump `actions/setup-node` 4 → 6** — ([#4048](https://github.com/diegosouzapw/OmniRoute/pull/4048)) - **ci(deps): bump `actions/cache` 4.3.0 → 5.0.5** — ([#4047](https://github.com/diegosouzapw/OmniRoute/pull/4047)) - **ci(deps): bump `actions/github-script` 7 → 9** — ([#4046](https://github.com/diegosouzapw/OmniRoute/pull/4046)) - **ci(deps): bump `ossf/scorecard-action` 2.4.0 → 2.4.3** — ([#4045](https://github.com/diegosouzapw/OmniRoute/pull/4045)) - **ci(deps): bump `actions/upload-artifact` 4 → 7** — ([#4044](https://github.com/diegosouzapw/OmniRoute/pull/4044)) --- ## [3.8.27] — 2026-06-17 ### ✨ 新功能 - **feat(combos): 在导入界面宣传 Combo 能力(多模态/推理/缓存)** — 将 Combo 包导入客户端(LobeHub / OpenCode / VS Code,通过 `/v1/combos` 和 VS Code Combo 目录)后不再需要手动启用多模态/图像输入、推理和缓存。`projectCombo` 现在附加注册表派生的 `capabilities` 块,采用保守策略:仅当**所有**具体模型步骤均证明该能力时才声明 `multimodal`/`reasoning`(无法证明的嵌套 Combo 引用会丢弃这些能力,因为策略可能路由到任意成员),`caching` 反映 Combo 显式的上下文缓存保护设置(不会产生意外的提示缓存成本)。公开的 `/v1/combos` 默认映射 (#2300) 保持不变,除非调用者主动选择。([#3979](https://github.com/diegosouzapw/OmniRoute/issues/3979) — 感谢 @xenstar) - **feat(sse): 委托 Anthropic 上下文编辑支持 Claude (`clear_tool_uses`)** — Claude 请求现在可以将上下文裁剪卸载到 Anthropic 的服务端上下文管理 API(beta `context-management-2025-06-27`、`clear_tool_uses_20250919`),在上游而非本地修剪过期的工具调用轮次。该功能仅适用于 Claude(编辑在服务端执行);多服务商上下文裁剪仍由本地压缩引擎处理。([#4021](https://github.com/diegosouzapw/OmniRoute/pull/4021) — 感谢 @diegosouzapw) - **feat(sse): 真实的 LLMLingua-2 ONNX 压缩引擎(稳定版)** — LLMLingua-2 提示压缩引擎现在使用真实的本地 ONNX 模型(默认 TinyBERT、transformers.js + tfjs),经 VPS 验证后提升为稳定版,替换了之前的占位实现。([#4014](https://github.com/diegosouzapw/OmniRoute/pull/4014) — 感谢 @diegosouzapw) - **feat(compression): 捕获每个引擎的分析数据 + Lite schema 修复** — 压缩管线现在为历史分析持久化每个引擎的详细数据,使控制台能将节省量归因到堆叠管线中的每个引擎,同时修复了 Lite schema 不匹配的问题。([#4018](https://github.com/diegosouzapw/OmniRoute/pull/4018) — 感谢 @diegosouzapw) - **feat(dashboard): Combo Live 级联中显示真实熔断器状态 (U1b)** — Combo Live 级联视图现在将每个服务商的真实熔断器状态(CLOSED / OPEN / HALF_OPEN)以徽章形式展示,从 `/api/monitoring/health` 实时读取,而非从请求结果推断健康状态。([#4029](https://github.com/diegosouzapw/OmniRoute/pull/4029) — 感谢 @diegosouzapw) - **feat(openai): 模型发现时遵循自定义 base URL + 补全 openai/codex 定价** — 配置了自定义 base URL 的 OpenAI 格式服务商现在在模型发现(不仅是推理)时也遵循该 URL,openai/codex 定价表已补全。发现请求通过 SSRF 保护的外部 fetch 路由。([#4005](https://github.com/diegosouzapw/OmniRoute/pull/4005) — 感谢 @artickc) - **feat(observability): 捕获实际上游服务商请求** — 请求检查器现在记录发送到上游服务商的确切载荷(翻译后),使您能看到 OmniRoute 实际派发的内容,而非仅看到客户端原始请求。([#3941](https://github.com/diegosouzapw/OmniRoute/pull/3941) — 感谢 @rdself) - **feat(providers): 服务商认证可见性控制** — 添加在控制台中显示/隐藏服务商认证详情的控制,使凭证仅在需要时可见。([#3953](https://github.com/diegosouzapw/OmniRoute/pull/3953) — 感谢 @rdself) - **feat(providers): 服务商仪表盘的模型搜索过滤器** — 服务商控制台新增搜索过滤器,可快速筛选服务商的模型列表。([#3950](https://github.com/diegosouzapw/OmniRoute/pull/3950) — 感谢 @felipesartori) - **feat(compression): 印尼语 caveman 规则 + 语言包** — 为基于规则的压缩引擎添加印尼语 "caveman" 规则集和语言包。([#3975](https://github.com/diegosouzapw/OmniRoute/pull/3975) — 感谢 @Veier04) - **feat(dashboard): 侧边栏分组分隔符开关** — 控制台侧边栏现在可以开关分组分隔符,获得更简洁的导航布局。([#3971](https://github.com/diegosouzapw/OmniRoute/pull/3971) — 感谢 @rdself) - **feat(api): 本地 `@@om-usage` 命令支持缓存的按 key 用量** — API 客户端可发送恰好为 `@@om-usage` 的消息,在本地检索缓存的 Claude 风格用量数据,无需将 prompt 转发到上游服务商。由新的按 key 许可标志控制。([#4034](https://github.com/diegosouzapw/OmniRoute/pull/4034) — 感谢 @Witroch4) ### 🐛 问题修复 - **fix(opencode): 无论用户如何命名服务商,都将 OpenCode 会话 ID 转发到上游** — `OpencodeExecutor` 转发了 `x-opencode-会话/请求/project/client` 头,但 OpenCode CLI 仅在配置的 `providerID` **以** `"opencode"` 开头时才发送这些头。如果用户将 OmniRoute 添加为自定义服务商(如 `"omniroute"`),CLI 会发送 `x-会话-affinity` / `X-Session-Id`(两者携带相同的会话 ID),而执行器从未读取这些头——因此在实际的服务商命名场景下,会话元数据转发实际上是一段死代码。opencode 系列执行器现在回退到 `x-会话-affinity` / `X-Session-Id` 并映射到 `x-opencode-会话`(当客户端未直接发送该头时),使得对 `opencode.ai` 上游的会话连续性对任何服务商名称都有效(直接发送的 `x-opencode-会话` 仍然优先)。仅限于此执行器——通用 `DefaultExecutor` 故意**不**这样做,以避免将客户端会话 ID 泄漏到任意第三方上游。([#4022](https://github.com/diegosouzapw/OmniRoute/issues/4022) — 感谢 @pizzav-xyz) - **fix(guardrails): Vision Bridge 在 describe 调用失败时不再丢弃图像(Nvidia NIM "Image unavailable")** — Vision Bridge 默认启用,并对 OmniRoute 无法从注册表证明其视觉能力的任何模型生效(`supportsVision !== true`,包括解析为 `null` 的未编目模型)。当每图像 describe 调用失败时(如未配置视觉模型),它用文字 `[Image N]: (unavailable)` 替换图像并丢弃原始 `image_url`——因此真正具有视觉能力的上游(Nvidia NIM)只收到文本并回答 "Image unavailable. Cannot provide description without visual data."。describe 失败不再具有破坏性:`replaceImageParts` 现在对失败图像接收 `null` 并**保留原始图像部分**,使上游仍能看到它(成功的 describe 仍用文本描述替换图像;`meta.descriptions` 可观测性不变)。([#4012](https://github.com/diegosouzapw/OmniRoute/issues/4012) — 感谢 @daniij) - **fix(kiro): 在 Kiro 流式路径上保留 `finish_reason: "tool_calls"`** — 通过 Kiro (Responses API) 服务商的流式 tool-call 请求,其终端 `finish_reason` 被报告为 `"stop"` 而非 `"tool_calls"`,因此 Agent 客户端(Hermes)将 tool-call 轮次视为已完成轮次,从未运行工具,下一个请求因不完整的工具状态而失败并返回 HTTP 400。`convertKiroToOpenAI` 的终端 `messageStopEvent`/`done` 分支硬编码了 `finish_reason: "stop"`,无论流是否发出了 `toolUseEvent`。翻译器现在在发出 tool-use 块时记录 `state.sawToolUse`,并在流产生了工具调用时在终端块(和 `state.finishReason`)中报告 `finish_reason: "tool_calls"`。非流式路径此前已正确。([#3980](https://github.com/diegosouzapw/OmniRoute/issues/3980) — 感谢 @lordavadon2) - **fix(resilience): 正确处理以数字纪元形式存储的连接冷却期** — 路由器持续向仍在速率限制冷却期内的连接派发请求,因为 `rate_limited_until`(`TEXT` 列)以原始纪元数字持久化,SQLite 将其强制转换为字符串如 `"1781696905131.0"`,而 `new Date(...)` 将其解析为 `NaN`,因此冷却中的连接从未被跳过。冷却读取谓词现在通过共享的 `cooldownUntilMs()` 辅助函数规范化数字纪元字符串;ISO 行为不变。([#3995](https://github.com/diegosouzapw/OmniRoute/pull/3995) — 感谢 @diegosouzapw) - **fix(providers): 为 LLM7 和 BytePlus 获取线上 `/models` 目录** — 导入 LLM7 或 BytePlus key 仅显示一个过时的小型硬编码列表,因为这两个服务商均未被模型导入路由的任何实时获取分支分类。两者现在均加入 `NAMED_OPENAI_STYLE_PROVIDERS`,因此路由使用该 key 探测 `/模型` 并提供线上目录,仅在上游获取失败时回退到本地目录。([#3996](https://github.com/diegosouzapw/OmniRoute/pull/3996) — 感谢 @FerLuisxd / @diegosouzapw) - **fix(dashboard): 日志自动刷新读取实时可见性状态,而非过时的挂载引用** — 当标签页在后台加载时,日志页面从未自动刷新,因为自动刷新间隔在每个 tick 上受挂载时一次性播种的可见性引用控制;tick 现在读取实时的 `document.visibilityState`,因此轮询在标签页可见后立即自愈,同时真正隐藏时仍然暂停。([#3997](https://github.com/diegosouzapw/OmniRoute/pull/3997) — 感谢 @tjengbudi / @diegosouzapw) - **fix(combo): 打乱 strict-random 容灾余量以分散负载** — 使用 `strict-random` 策略时,持续失败的模型几乎在每次请求上都重试,因为只有牌组选中的 slot 0 被洗牌,而容灾余量保持固定优先级顺序;余量现在也被洗牌,因此容灾负载(及从失败目标恢复)均匀分散到健康对等节点。([#3998](https://github.com/diegosouzapw/OmniRoute/pull/3998) — 感谢 @KeNJiKunG / @diegosouzapw) - **fix(claude): 在 Claude OAuth 路径上转发客户端 `tool-search-tool-2025-10-19` anthropic-beta** — 启用延迟工具时,Claude Code 协商 `tool-搜索-tool-2025-10-19` beta,但 OmniRoute 在两个 Claude 代码路径上均将其丢弃,因此 claude.ai 后端以 `400 Tool reference not found` 拒绝每个延迟工具请求。新的白名单合并 (`mergeClientAnthropicBeta`) 现在在两个路径上将客户端协商的 beta 合并到出站集合中,仅追加白名单中的客户端 beta(保留 #3415 修复)。([#3999](https://github.com/diegosouzapw/OmniRoute/pull/3999) — 感谢 @huohua-dev / @diegosouzapw) - **fix(executor): 在非流式请求中去除 `stream_options`(NVIDIA NIM 400)** — 无论 `流` 如何都发送 `stream_options: { include_usage: true }` 的客户端(如 OpenAI Python SDK)在非流式调用中将其原样传递,NVIDIA NIM 以 `400 "Stream options can only be defined when 流=True"` 拒绝。`DefaultExecutor.transformRequest` 现在在 `流` 为 false 时去除 `stream_options`;流式注入路径不变。([#4000](https://github.com/diegosouzapw/OmniRoute/pull/4000) — 感谢 @andrea-kingautomation / @daniij / @diegosouzapw) - **fix(sse): 在 `getUnsupportedParams` (mimocode) 中保护无模型注册表条目** — 没有模型映射的注册表条目 (mimocode) 在计算不支持参数时抛出异常;查找现在保护无模型情况,使请求验证不再崩溃。([#4015](https://github.com/diegosouzapw/OmniRoute/pull/4015) — 感谢 @diegosouzapw) - **fix(perplexity-web): 解析结构化 `diff_block` 流使答案不再为空** — Perplexity web 将其答案作为 RFC-6902 `diff_block` 补丁流式传输,OmniRoute 在 `PENDING` 阶段未应用这些补丁,因此响应返回为空;解析器现在应用补丁并在 `COMPLETED` 时物化文本。([#4001](https://github.com/diegosouzapw/OmniRoute/pull/4001) — 感谢 @artickc) - **fix(default-executor): 遵循 OpenAI 格式服务商的自定义 `providerSpecificData.baseUrl`** — 配置了自定义 base URL 的 OpenAI 格式服务商在推理路径上被忽略;默认执行器现在遵循 `providerSpecificData.baseUrl`,使请求到达配置的端点。([#4002](https://github.com/diegosouzapw/OmniRoute/pull/4002) — 感谢 @artickc) - **fix(live-ws): 将 LiveWS sidecar 事件桥接到控制台** — LiveWS sidecar 发出的事件未到达控制台;它们现在被桥接,使实时 websocket 活动可见。(sidecar 认证 Token 解析中的 cookie 认证回归也已修正。)([#4004](https://github.com/diegosouzapw/OmniRoute/pull/4004) — 感谢 @megamen32) - **fix(qwen-web): cookie 验证误报 — 检查响应 body 中的用户对象** — Qwen web cookie 验证将有效 cookie 报告为无效;现在检查响应 body 中的 `user` 对象,而非仅依赖状态码。([#3958](https://github.com/diegosouzapw/OmniRoute/pull/3958) — 感谢 @thezukiru) - **fix(vision-bridge): 强制 tokenrouter DeepSeek 模型使用桥接** — tokenrouter DeepSeek 模型现在强制通过 Vision Bridge,使图像输入得到正确处理。([#3946](https://github.com/diegosouzapw/OmniRoute/pull/3946) — 感谢 @WormAlien) - **fix(api): 对 `/api/auth/login` 上格式错误的 JSON 返回 400(而非 500)** — 登录端点上的格式错误 JSON body 返回不透明的 500;现在返回正确的 400。([#4031](https://github.com/diegosouzapw/OmniRoute/pull/4031) — 感谢 @rdself) - **fix(dashboard): Playground 对比标签页加载 + HTTP 方法守卫** — Playground 对比标签页加载失败;加载路径已修复并添加了 HTTP 方法守卫。([#4024](https://github.com/diegosouzapw/OmniRoute/pull/4024) — 感谢 @rdself) - **fix(proxy): 在特性标志后控制控制平面代理直接回退(fail-closed)** — 当固定代理不可达时,控制平面操作的直接连接容灾现在受特性标志控制并 fail-closed,因此固定代理永远不会被静默绕过,除非显式允许。([#3963](https://github.com/diegosouzapw/OmniRoute/pull/3963) — 感谢 @rdself) - **fix(db): 持久化备份保留天数** — 备份保留天数设置在重启后未持久化;现在被持久存储。([#3970](https://github.com/diegosouzapw/OmniRoute/pull/3970) — 感谢 @rdself) - **fix(dashboard): 优化服务商配额卡片显示** — 服务商配额卡片布局经过优化,呈现更清晰的配额/用量��示。([#3969](https://github.com/diegosouzapw/OmniRoute/pull/3969) — 感谢 @rdself) - **fix(dashboard): 优化压缩设置、存储标签和侧边栏分组** — 打磨压缩设置 UI、澄清存储标签、整理侧边栏分组。([#4033](https://github.com/diegosouzapw/OmniRoute/pull/4033) — 感谢 @rdself) ### 🔒 安全与加固 - **fix(security): 消除 combo `` 标签正则中的多项式 ReDoS** — `comboAgentMiddleware` 的缓存标签模式将标签包裹在无界换行运行中 (`(?:\n|\r)*`),使 `.test()` / `.replace()` 在大量换行的输入上以 O(n²) 运行(CodeQL `js/polynomial-redos`)。检测模式现在仅匹配核心 ``,全局剥离模式限制周围的换行运行,保持线性;检测/提取/多标签剥离行为不变。([#3982](https://github.com/diegosouzapw/OmniRoute/pull/3982) — 感谢 @diegosouzapw) - **ci(security): 加固工作流 — artipacked `persist-凭证`、缓存投毒、SC2086** — GitHub Actions 工作流针对 artipacked `persist-凭证` 泄漏和缓存投毒进行了加固,并修复了 shell 引用 (`SC2086`) 问题。([#3965](https://github.com/diegosouzapw/OmniRoute/pull/3965) — 感谢 @diegosouzapw) - **ci(quality): 将 require-tighten + OSV + Trivy 转为阻断(周期结束)** — 每个模块的 require-tighten 检查和 OSV / Trivy 扫描器从建议转为阻断,用于 v3.8.27 周期收尾,使新的依赖或覆盖率回退导致 CI 失败。([#3984](https://github.com/diegosouzapw/OmniRoute/pull/3984) — 感谢 @diegosouzapw) - **chore(deps): dependabot 安全升级 + 移除未使用的 gray-matter** — 应用一批 Dependabot 安全升级,并从依赖树中移除未使用的 `gray-matter`。([#4036](https://github.com/diegosouzapw/OmniRoute/pull/4036) — 感谢 @diegosouzapw) - **chore(deps): 自动依赖升级** — Dependabot 升级了生产依赖组(13 个更新)、`vite`、`form-data` 和 `npm_and_yarn` 组。([#3915](https://github.com/diegosouzapw/OmniRoute/pull/3915), [#3942](https://github.com/diegosouzapw/OmniRoute/pull/3942), [#3943](https://github.com/diegosouzapw/OmniRoute/pull/3943), [#3944](https://github.com/diegosouzapw/OmniRoute/pull/3944) — 感谢 @dependabot) ### 🧹 内部 / 质量 / 文档 - **feat(ci): Quality Gate v2 — Onda 0 + Onda 1** — Quality Gate v2 计划的前两波:门控翻转、测试影响分析 (TIA)、SAST、DAST-smoke 和变异测试基础设施。([#4016](https://github.com/diegosouzapw/OmniRoute/pull/4016) — 感谢 @diegosouzapw) - **refactor: 将服务商注册表模块化为独立的服务商插件** — `providerRegistry.ts` 拆分为独立的按服务商插件模块(非堆叠)。后续修复恢复了迁移中丢失的 `byteplus` + `mimocode` 模块。([#3993](https://github.com/diegosouzapw/OmniRoute/pull/3993) — 感谢 @oyi77 / @diegosouzapw) - **refactor: 模块化 schemas(非堆叠)** — 请求/响应 schema 定义拆分为独立模块,减少文件大小并提高可维护性。([#3988](https://github.com/diegosouzapw/OmniRoute/pull/3988) — 感谢 @oyi77) - **fix: 恢复因 schema/注册表模块化导致的单元测试回归** — schema/注册表模块化 (#3988, #3993) 静默丢弃了被单元测试覆盖的内部逻辑;本 PR 恢复了受影响的单元测试。([#4030](https://github.com/diegosouzapw/OmniRoute/pull/4030) — 感谢 @diegosouzapw) - **refactor(dashboard): 设置 UI 布局 + API Keys 命名** — 设置 UI 布局已重组,"API Keys" 命名已明确。([#4020](https://github.com/diegosouzapw/OmniRoute/pull/4020) — 感谢 @rdself) - **大量UI显示和i18n优化 (dashboard UI display + i18n improvements)** — 一批控制台 UI 显示优化和 i18n 字符串改进。([#3973](https://github.com/diegosouzapw/OmniRoute/pull/3973) — 感谢 @rdself) - **fix(ci): 将 TIA 作用域限制为仅 `node:test` 单元文件** — 测试影响分析匹配了 `node:test` 运行器不执行的文件,产生 99 个误报失败;TIA glob 现在与 `test:unit` glob 完全一致。([#4035](https://github.com/diegosouzapw/OmniRoute/pull/4035) — 感谢 @diegosouzapw) - **fix(ci): electron-release publish-npm needs `contents: write`** — the reusable npm-publish job invoked by the electron release lacked `contents: write`, causing a v3.8.26 `startup_failure`; the permission was granted. ([#3966](https://github.com/diegosouzapw/OmniRoute/pull/3966) — 感谢 @diegosouzapw) - **test(opencode-plugin): ESM default-export test (drop the stale CJS bundle test)** — replaces the stale CJS bundle test with an ESM 默认-export test, following up the #3883 ESM-only 迁移. ([#3967](https://github.com/diegosouzapw/OmniRoute/pull/3967) — 感谢 @diegosouzapw) - **fix(ci): Fix promptfoo security-assertion parsing** — the promptfoo (DAST/security eval) 断言 parser was corrected so security assertions are read reliably. ([#4032](https://github.com/diegosouzapw/OmniRoute/pull/4032) — 感谢 @rdself) - **docs(troubleshooting): note that the MITM proxy cannot intercept Windows-host apps under WSL** — documents that the MITM 代理 running inside WSL cannot intercept traffic from apps on the Windows host. ([#4003](https://github.com/diegosouzapw/OmniRoute/pull/4003) — 感谢 @diegosouzapw) - **chore(quality): maintenance roll-up** — assorted quality-gate hygiene that 不 change runtime behavior: re-baseline `validation.ts` for the #3958 qwen body-check, allowlist the `socks` dependency declared by #4004, ignore jscpd major bumps (the v5 Rust rewrite breaks the pinned duplication gate), untrack an accidentally-committed root `node_modules` symlink (and gitignore it), rehome the #3972 logs auto-refresh test so a runner collects it, and open the v3.8.27 development cycle. (thanks @diegosouzapw) --- ## [3.8.26] — 2026-06-15 ### ✨ 新功能 - **feat(media): Vertex AI (Google) 语音合成、转录、音乐和视频生成** — Vertex AI 的 Google 媒体模型现在可通过动态发现进行路由:语音合成、音频转录、音乐生成和视频生成。([#3929](https://github.com/diegosouzapw/OmniRoute/pull/3929) — 感谢 @artickc) - **feat(glm): 添加 GLM-5.2 的 effort-tier 路由 (high/max)** — GLM-5.2 已注册 high/max effort-tier 路由。([#3885](https://github.com/diegosouzapw/OmniRoute/pull/3885) — 感谢 @dhaern) - **feat(combo): 添加粘性轮询目标限制** — 轮询 combo 可限制会话内保持 "粘性" 的目标数量 (`stickyRoundRobinLimit`),在粘性和分散之间取得平衡。([#3846](https://github.com/diegosouzapw/OmniRoute/pull/3846) — 感谢 @adivekar-utexas) - **feat(openrouter): 连接预设** — OpenRouter 连接支持可重用预设(服务商路由/排序/量化偏好),添加连接时可选择。([#3878](https://github.com/diegosouzapw/OmniRoute/pull/3878) — 感谢 @rdself) ### 🐛 问题修复 - **fix(compression/memory): 停止记忆 + 压缩污染上游提示缓存** — 启用压缩和/或记忆时,对缓存服务商(Anthropic 系列)的请求在每个轮次上都错过提示缓存,导致成本倍增。两个根本原因:(1) 记忆注入将检索到的记忆(**因用户查询而异**)前置到消息数组的索引 0,使整个可缓存前缀在每个轮次上发生偏移;记忆现在在请求携带 `cache_control` 断点时,插入到最后一条用户消息之前,保持可缓存前缀(系统提示 + 先前轮次)字节稳定。(2) 由 `getCacheAwareStrategy()` 计算的缓存感知 `skipSystemPrompt` 标志被 `selectCompressionStrategy()` 丢弃(该函数只能返回模式),因此系统提示在缓存下仍可能被压缩;新的 `resolveCacheAwareConfig()` 现在强制为缓存请求开启 `preserveSystemPrompt`。([#3936](https://github.com/diegosouzapw/OmniRoute/pull/3936),关闭 [#3890](https://github.com/diegosouzapw/OmniRoute/issues/3890) — 感谢 @xenstar / @diegosouzapw) - **fix(providers): 注册 BytePlus ModelArk 使 API key 可添加** — 添加 BytePlus (`ark-…`) key 时报 "invalid"。`byteplus` 存在于服务商目录 (`APIKEY_PROVIDERS`) 中但**从未在路由注册表中注册**,因此 key 验证回退到 `{ unsupported: true }` → HTTP 400 → UI 将所有 key 渲染为无效(且该服务商无法用于推理)。新增注册表条目,仿照现有 Volcengine Ark 服务商:OpenAI 兼容格式、基础 URL `https://ark.ap-southeast.bytepluses.com/api/v3`(区域 `ap-southeast-1`)、`Authorization: Bearer` 认证,使用目录中公告的模型(Seed 2.0、Kimi K2 Thinking、GLM 4.7、GPT-OSS-120B)作为种子。([#3935](https://github.com/diegosouzapw/OmniRoute/pull/3935),关闭 [#3877](https://github.com/diegosouzapw/OmniRoute/issues/3877) — 感谢 @nikohd12 / @diegosouzapw) - **fix(providers): Nous Research key validation no longer fails on a stale probe model** — adding a valid Nous Research API Key reported "invalid" even though the same key worked via the portal's copy-shell `curl`. The validation probe sent `模型: "nousresearch/hermes-4-70b"`, which Nous 不 serve, so the API returned `400` and the validator (which only treated `200`/`429` as success) reported the key invalid. The probe now uses the real `Hermes-4-70B` slug, and any non-认证 4xx (`400`/`404`/`422`) is treated as a valid key (the 请求 shape was wrong, not the 凭证) — mirroring the longcat/nvidia validators so a future 模型 rename can't re-break key validation. ([#3934](https://github.com/diegosouzapw/OmniRoute/pull/3934), closes [#3881](https://github.com/diegosouzapw/OmniRoute/issues/3881) — 感谢 @FerLuisxd / @diegosouzapw) - **fix(stream): 持久化流中途的上游失败** — 当上游流中途失败时,部分响应和增量用量现在被完成并持久化,而非丢失;提取了共享的 `streamFailureFinalization` 路径,并合并了增量 Claude 用量(#3879 的跟进)。([#3937](https://github.com/diegosouzapw/OmniRoute/pull/3937) — 感谢 @rdself) - **fix(perplexity-web): 将请求载荷更新为 schema v2.18(HTTP 400)** — Perplexity web 请求开始返回 HTTP 400;请求载荷已更新为 Perplexity v2.18 schema。([#3938](https://github.com/diegosouzapw/OmniRoute/pull/3938) — 感谢 @artickc) - **fix(stream): 保持进行中请求载荷同步** — 按 ID 挂起的请求记录现在原地更新 (`Object.assign`),使进行中载荷与已派发的内容保持一致(与 #3937 共存)。([#3940](https://github.com/diegosouzapw/OmniRoute/pull/3940) — 感谢 @rdself) - **fix: 稳定推理流和请求日志** — 推理 token 流式传输和请求日志捕获路径已稳定,避免丢弃/重复推理帧和不一致的日志条目。([#3879](https://github.com/diegosouzapw/OmniRoute/pull/3879) — 感谢 @rdself) - **fix(opencode-plugin): 在 LCD 上下文窗口中包含嵌套 Combo 引用** — OpenCode 插件现在在计算最小公分母上下文窗口时跟随嵌套的 Combo 引用,使嵌套在其他 Combo 中的 Combo 不再报告膨胀的窗口。([#3910](https://github.com/diegosouzapw/OmniRoute/pull/3910) — 感谢 @herjarsa) - **fix(models): 修正失败模型自动隐藏的默认值** — 控制失败模型何时自动隐藏的默认值已修正,自动隐藏现在为主动选择加入,使模型不再意外被隐藏。([#3930](https://github.com/diegosouzapw/OmniRoute/pull/3930) — 感谢 @rdself) - **fix(openrouter): 编辑连接时显示预设字段** — 连接预设字段仅在创建连接时出现,编辑时不显示;现在两者均显示(#3878 的跟进)。([#3921](https://github.com/diegosouzapw/OmniRoute/pull/3921) — 感谢 @rdself) - **fix(sse): 在首个 delta 上声明助手角色(Responses→Chat)** — Responses-API→Chat-Completions 流的首个 SSE delta 现在携带 `role: "assistant"`,严格的 OpenAI 兼容客户端在内容 delta 之前需要该字段。([#3911](https://github.com/diegosouzapw/OmniRoute/pull/3911) — 感谢 @diego-anselmo) - **fix(vertex): 添加 generative-language 作用域使 SA-JSON 模型发现正常工作** — Vertex 服务账户 (SA-JSON) 模型发现在缺少 `generative-language` OAuth 作用域时失败;该作用域现在被请求。([#3922](https://github.com/diegosouzapw/OmniRoute/pull/3922) — 感谢 @artickc) - **fix(proxy): 固定代理不可达时控制平面操作回退到直接连接** — 控制平面操作(验证、发现)现在在连接固定代理不可达时容灾到直接连接,而非直接失败。([#3906](https://github.com/diegosouzapw/OmniRoute/pull/3906) — 感谢 @zhiru) - **fix(providers): 防止 zai/glm 僵尸 socket 挂起并收紧默认 keepAlive** — zai/glm 可能在失效的 keep-alive socket 上挂起;默认 keepAlive 已收紧以驱逐僵尸 socket。([#3907](https://github.com/diegosouzapw/OmniRoute/pull/3907) — 感谢 @insoln) - **fix(setup): 从 setup-open-code 中移除过时的 CJS bundle 检查** — OpenCode 设置助手不再检查 CJS bundle,因为现在仅 ESM 的插件已不再包含它。([#3908](https://github.com/diegosouzapw/OmniRoute/pull/3908) — 感谢 @herjarsa) - **fix(opencode-plugin): 移除 CJS bundle 以修复 OpenCode 插件加载器** — 插件现在仅 ESM,修复了因双 CJS/ESM 构建而失败的 OpenCode 加载器。([#3883](https://github.com/diegosouzapw/OmniRoute/pull/3883) — 感谢 @herjarsa) - **fix(mcp): better-sqlite3 绑定缺失时回退到 `node:sqlite`** — MCP 服务器现在在原生的 better-sqlite3 绑定不可用时回退到 Node 内置的 `node:sqlite`,而非崩溃。([#3887](https://github.com/diegosouzapw/OmniRoute/pull/3887) — 感谢 @megamen32) - **fix(models): 修正 generate-models 别名查找** — 模型生成期间的别名解析已修正,使别名模型 ID 能解析到其规范条目。([#3870](https://github.com/diegosouzapw/OmniRoute/pull/3870) — 感谢 @YunyunZhai) - **fix(combo): 保护候选池防止空数组** — Combo 候选池选择在池解析为空数组时不再抛出异常。([#3871](https://github.com/diegosouzapw/OmniRoute/pull/3871) — 感谢 @YunyunZhai) ### 🔒 安全与加固 - **fix(security): 升级 form-data + vite(2 个 HIGH),加固工作流模板注入并白名单守卫 `workflow_run`** — 两个 HIGH Dependabot 建议(`表单-data`、`vite`)已升级;GitHub Actions 工作流针对 `${{ }}` 模板注入进行了加固(不受信任的值现在通过 `env:` 传递);受保护的 `workflow_run` 触发器已加入白名单。([#3949](https://github.com/diegosouzapw/OmniRoute/pull/3949) — 感谢 @diegosouzapw) ### 🧹 内部 / 质量 / 文档 - **fix(ci): 为 npm 发布任务授予 `contents: write` 权限以附加 SBOM** — v3.8.25 TokenPermissions 加固将 npm-publish `publish` 任务设为 `contents: read`,但其 "Attach SBOM to GitHub Release" 步骤 (`gh release upload`) 需要 `contents: write`,在 v3.8.25 发布中因 HTTP 403 失败(npm / GitHub Packages / opencode-插件 / Docker / Electron 均正常发布;仅 SBOM 附加失败 — v3.8.25 SBOM 已手动附加)。([#3874](https://github.com/diegosouzapw/OmniRoute/pull/3874) — 感谢 @diegosouzapw) - **ci(quality): 使 zizmor / gitleaks / OSV 扫描器功能可用 + 冻结建议基线** — 供应链扫描器现在实际执行(正确的安装 + 调用),并冻结建议基线,使新发现作为差异呈现。([#3947](https://github.com/diegosouzapw/OmniRoute/pull/3947) — 感谢 @diegosouzapw) - **ci(quality): 修复扫描器安装 + size-limit 预设,将 `codeqlAlerts` 从建议提升为阻断** — 修正了扫描器安装和 size-limit 预设,将 `codeqlAlerts` 从建议提升为阻断。([#3945](https://github.com/diegosouzapw/OmniRoute/pull/3945) — 感谢 @diegosouzapw) - **ci(quality): 接入 Stryker 变异测试(建议,每夜运行)** — Stryker 变异测试每夜运行(建议)— Quality Gates Fase 7 · Task 11。([#3898](https://github.com/diegosouzapw/OmniRoute/pull/3898) — 感谢 @diegosouzapw) - **ci(quality): 冻结每个模块的覆盖率底线 + 接入 require-tighten(建议)** — 每个模块的覆盖率底线已冻结,建议的 "require-tighten" 检查标记低于底线的模块。([#3901](https://github.com/diegosouzapw/OmniRoute/pull/3901) — 感谢 @diegosouzapw) - **ci(quality): 在 `check-known-symbols` 上强制执行过期白名单检查** — 过期的白名单条目(抑制不再存在的符号)现在使门控失败 — Fase 6A.3 跟进。([#3899](https://github.com/diegosouzapw/OmniRoute/pull/3899) — 感谢 @diegosouzapw) - **test(ci): 通过每次测试重新播种 + 真实重置来消除管线载荷测试的不稳定性** — 管线载荷测试套件现在每次测试重新播种并执行真实的缓存重置,消除了跨测试顺序的不稳定性。([#3893](https://github.com/diegosouzapw/OmniRoute/pull/3893) — 感谢 @diegosouzapw) - **fix(ci): 从 nightly-llm-security 中移除任务级 `if` 中的 `secrets` 引用** — 在任务级 `if` 中引用 `secrets` 导致推送时出现 `startup_failure`;门控已移动使工作流正常启动。([#3892](https://github.com/diegosouzapw/OmniRoute/pull/3892) — 感谢 @diegosouzapw) - **test: 在 #3907 源端回退后将 runtime-timeouts keepAlive 基线调整为 4000** — keepAlive 断言在 #3907 源端回退后已与源值 (4000) 重新对齐。([#3933](https://github.com/diegosouzapw/OmniRoute/pull/3933) — 感谢 @diegosouzapw) - **chore(repo): 将质量门控状态嵌套到 `config/quality` 下,清理仓库根目录** — 基线/白名单/指标移至 `配置/quality/` 下,减少根目录跟踪文件数量。([#3896](https://github.com/diegosouzapw/OmniRoute/pull/3896) — 感谢 @diegosouzapw) - **docs: 将服务商计数刷新为 226 + 重新生成 `PROVIDER_REFERENCE.md`** — README 中声明了过时的 `177 服务商`;规范生成器 (`scripts/docs/gen-服务商-reference.ts`) 现在报告 **226 个唯一服务商 ID**,因此 README 徽章/锚点和生成的服务商参考已同步。同时添加了文档审计/同步报告。(感谢 @diegosouzapw) - **docs: 将所有文档同步到 v3.8.24 + 计数守卫和 wiki/prose CI** — 完整文档同步,包含严格的服务商/语言包计数守卫以及 Vale / markdownlint prose CI。([#3804](https://github.com/diegosouzapw/OmniRoute/pull/3804) — 感谢 @diegosouzapw) - **docs: 重新生成过时的计数为规范值** — 226 个服务商 / 87 个 MCP 工具 / 15 个策略 / 42 个语言。([#3904](https://github.com/diegosouzapw/OmniRoute/pull/3904) — 感谢 @diegosouzapw) - **docs(quality): 修正过时的门控计数 + 添加可选的 agent-lsp 脚手架** — ([#3902](https://github.com/diegosouzapw/OmniRoute/pull/3902) — 感谢 @diegosouzapw) - **docs(mcp): 修正 MCP 工具清单图表源 + 文本为 87 个工具** — ([#3909](https://github.com/diegosouzapw/OmniRoute/pull/3909) — 感谢 @diegosouzapw) - **docs: 更新压缩章节为 9 引擎多层堆叠** — ([#3894](https://github.com/diegosouzapw/OmniRoute/pull/3894) — 感谢 @diegosouzapw) - **ci(docs): 自动化 GitHub wiki 同步(添加缺失页面 + 覆盖计数)** — ([#3900](https://github.com/diegosouzapw/OmniRoute/pull/3900) — 感谢 @diegosouzapw) - **docs: 要求每个开发任务使用独立的 git worktree + 分支(硬规则 #19)** — 在共享检出事故后,编撰了 worktree 隔离规则。([#3939](https://github.com/diegosouzapw/OmniRoute/pull/3939) — 感谢 @diegosouzapw) - **fix(docs): 添加 MDX frontmatter 到 `DOCUMENTATION_AUDIT_REPORT` 使 fumadocs 构建通过** — 审计报告缺少 MDX 页面所需的 `title:` frontmatter。(感谢 @diegosouzapw) --- ## [3.8.25] — 2026-06-14 ### ✨ 新功能 - **feat(compression): 可插拔压缩引擎 + 异步管线 + Compression Studios** — 一个全新的提示-压缩子系统,支持可选择的引擎(Lite / Aggressive / Ultra)、接入聊天核心的异步压缩管线,以及用于检查和调优压缩的 "Compression Studios" 工具。 ([#3848](https://github.com/diegosouzapw/OmniRoute/pull/3848)) - **feat(compression-ui): 统一压缩配置界面** — 新增 Compression Hub,包含每个引擎的子页面(Lite / Aggressive / Ultra)、组合编辑器、专用侧边栏入口以及默认开启的实时 WebSocket。 ([#3860](https://github.com/diegosouzapw/OmniRoute/pull/3860)) - **feat(security): 覆盖所有 LLM 路由的提示注入守卫 + 红队测试套件** — 提示注入守卫现已覆盖所有 LLM 路由(chat、responses、embeddings、images、audio、rerank、搜索、moderations、videos、music),配备共享输入清洗器以及基于 promptfoo 的红队测试套件(Quality Gates Fase 8 · Bloco D)。 ([#3857](https://github.com/diegosouzapw/OmniRoute/pull/3857)) - **feat(kiro): 每个账户的实时模型发现** — Kiro 现通过 CodeWhisperer `ListAvailableModels`(按区域匹配,带静态目录容灾)发现每个账户/层级的授权模型。 ([#3836](https://github.com/diegosouzapw/OmniRoute/pull/3836) — 感谢 @artickc) - **feat(gemini/vertex): 在动态发现中展示 Veo 视频模型** — Veo 视频模型(`predictLongRunning`)现出现在 Gemini/Vertex 动态模型发现中。 ([#3839](https://github.com/diegosouzapw/OmniRoute/pull/3839) — 感谢 @artickc) - **feat(mimocode): 多账户轮询的每账户代理** — 每个 mimocode 账户可通过其专属代理进行路由(按账户指纹通过 `runWithProxyContext` 解析),并配备 "分发代理" 界面辅助工具。 ([#3837](https://github.com/diegosouzapw/OmniRoute/pull/3837) — 感谢 @pizzav-xyz) - **feat(intelligence): 将 Arena ELO 同步作为功能开关暴露** — LM Arena ELO 排行榜同步现已可切换(`ARENA_ELO_SYNC_ENABLED`,DB 覆盖 + 环境变量容灾)。 ([#3821](https://github.com/diegosouzapw/OmniRoute/pull/3821) — 感谢 @rdself) ### 🐛 问题修复 - **test(oauth): 验证真实 gemini-cli / antigravity 分发路径的 refresh_token 保留** — #3679/#3766 回归测试使用了一个合成服务商,该服务商通过通用的 `tokenUrl` 路径进行路由,因此该修复从未针对实际 Google 系列服务商进行过验证——后者通过 `refreshGoogleToken()` 针对硬编码的 `OAUTH_ENDPOINTS.google.token` 进行分发。新增一个测试,通过真实的 `gemini-cli`/`antigravity` 路径驱动 `checkConnection`(将 Google 令牌端点重定向到返回 `invalid_grant` 的本地服务器),并断言 `refresh_token` 被保留(而非置空)——从而确认这些连接不会在刷新失败时被错误地销毁。 ([#3850](https://github.com/diegosouzapw/OmniRoute/issues/3850) — 感谢 @3xa228148) - **fix(oauth): 为 GitLab Duo 提供清晰的设置提示,而非 "Internal server error"** — 在没有注册 OAuth 客户端的情况下添加 GitLab Duo 连接,在添加连接步骤中会返回一个含糊的 `Internal server 错误`。当 `GITLAB_DUO_OAUTH_CLIENT_ID` 缺失时,`buildAuthUrl` **抛出异常**,路由将其吞并为通用 500 错误。现返回 `null`(与 Qoder 服务商行为一致),授权路由会显示可操作的提示:在 `https://gitlab.com/-/profile/applications` 注册 OAuth 应用,重定向 URI 为 `http://localhost:20128/callback`,权限范围为 `ai_features read_user`,然后设置 `GITLAB_DUO_OAUTH_CLIENT_ID`。 ([#3861](https://github.com/diegosouzapw/OmniRoute/issues/3861) — 感谢 @sidinsearch) - **fix(db): 持久化"保留最近备份"设置** — 在设置 → 数据库备份保留中更改备份保留数量无效:刷新后总是跳回 20(并且启动后编辑 `.env` 也被忽略,因为 `process.env` 不会被重新加载)。`getDbBackupMaxFiles()` 仅读取 `DB_BACKUP_MAX_FILES` 环境变量——没有 setter 也没有持久化值。该值现通过专用的 `key_value` 存储进行往返(`getDbBackupMaxFiles` 优先级:环境变量覆盖 → 持久化的界面值 → 默认 20),并且"清理旧备份"操作会持久化所选数量。现有安装保留历史默认值 20,直到显式更改。 ([#3834](https://github.com/diegosouzapw/OmniRoute/issues/3834) — 感谢 @netstratego) - **fix(sse): 将 Gemini 思考预算限制在模型真实上限内(`reasoning_effort`/`effort=high` 400)** — 将 OpenAI `reasoning_effort=high`(以及 Claude-Code `output_config.effort=high`)翻译到 Gemini 目标时,发送了硬编码的 `thinkingBudget: 32768`,这超过了 Flash 级别 Gemini 的真实上限 24576 → 上游 HTTP 400(`thinkingLevel=high` 路径已经使用 24576 并且在相同模型上正常工作)。`gemini-2.5-flash` 现声明其真实 `thinkingBudgetCap`(24576),使现有的 `capThinkingBudget()` 截断点真正生效,并且 Claude→Gemini 的 `output_config.effort` 路径——此前直接发送原始值完全没有截断——现通过相同的截断点(pro 级别,真实上限 32768,保持不变)。 ([#3842](https://github.com/diegosouzapw/OmniRoute/issues/3842) — 感谢 @andrea-kingautomation) - **fix(intelligence): 从实时启动路径运行定价 + models.dev 同步** — 与 Arena ELO 同步(v3.8.24)类似,外部**定价同步**(`PRICING_SYNC_ENABLED`)和**模型.dev 能力同步**(设置 → AI 开关)仅从 `server-init.ts` 初始化,而 Next 独立运行时永远不会执行该文件——并且模型.dev 完全没有调用者。它们的开关在生产环境中无效。两者现从 `instrumentation-node.ts` 初始化(自门控、保留可选择性、非阻塞、永不致命)。(感谢 @diegosouzapw) - **test(proxy): 保护每连接"直连"绕过全局代理 + 更清晰的标签** — 每连接"代理关闭"开关(`proxyEnabled: false`)已经覆盖了已配置的**全局**代理(`resolveProxyForConnection` 在全局步骤之前短路到 `level: "direct"`)。新增一个显式回归测试,证明该绕过优先于全局分配(并在重新启用时可往返),并将界面标签改为"直连(绕过代理)",以便运维人员识别。关闭 [#2996](https://github.com/diegosouzapw/OmniRoute/issues/2996) 中的验证缺口。(感谢 @diegosouzapw) - **feat(connections): 每连接"禁用冷却"退出机制** — 连接现可选择退出瞬时冷却(`providerSpecificData.disableCooling`,在编辑连接弹窗中带开关)。设置后,可恢复的故障仍会记录错误/退避,但**不会**将该连接移出轮转,因此它仍然有资格被选中——适用于你永远不希望因短暂波动而被搁置的主密钥。终端状态(封禁 / 过期 / 额度耗尽)仍然生效。 ([#2997](https://github.com/diegosouzapw/OmniRoute/issues/2997) — 感谢 @diegosouzapw) - **fix(combo): 恢复无会话组合粘性 + 推理感知就绪(v3.8.14 后的 504 / TPS 回归)** — #3399(v3.8.16)将 `` 标签组合固定替换为基于客户端 `sessionId` 门控的服务端上下文缓存固定。不发送会话 ID 的客户端(大多数 OpenAI 兼容工具)丢失了组合粘性,导致组合每轮都重新运行策略选择 → 上游提示缓存未命中 → 冷高推理启动(~78s)→ 间歇性 `[504] Upstream request did not return response headers` + TPS 崩溃(仅发生在组合上)。固定现在在没有会话 ID 时回退到稳定的每对话指纹(`extractSessionAffinityKey(body)`)——**仅当 `context_cache_protection` 开启时**,因此 #3399 的防泄漏行为得以保留。另外,流就绪窗口现在**无条件**为高推理 Codex GPT-5.x 授予 +30s 推理预算(小型高推理提示无论粘性如何,在 80s 基准下都会 504)。 ([#3825](https://github.com/diegosouzapw/OmniRoute/issues/3825) — 感谢 @bypanghu) - **test(combo): 覆盖 `skipProviderBreaker` 消费者门控** — 生产者已测试,但消费者(失败的 Combo 目标是否会触发全服务商熔断器)未测试;熔断器决策现为导出的纯谓词(`shouldRecordProviderBreakerFailure`,行为一致),并有直接测试断言 `connection_cooldown` 503 不会触发熔断器,而普通 503 会。关闭 [#2743](https://github.com/diegosouzapw/OmniRoute/issues/2743) 中的另一个延期测试缺口。(感谢 @diegosouzapw) - **fix(providers): 显示真实的 Devin 错误 + 修正 Windsurf 认证说明** — Devin 聊天返回通用 502 "Invalid SSE response for non-streaming request",吞没了真实原因(例如"Devin CLI not found"):仅含错误的 SSE 块(无 `choices`)现会传播其经过清洗的消息。Windsurf "Visit windsurf.com/show-auth-token" 说明(裸 URL 在没有 IDE 提供的 `?state=` 时不会显示令牌)现引导用户使用 `Windsurf: Provide Auth Token` 命令面板流程。 ([#3324](https://github.com/diegosouzapw/OmniRoute/issues/3324) — 感谢 @mikmaneggahommie) - **fix(grok-web): 针对反机器人 / IP 信誉拦截的更清晰 403 消息** — 从被标记的数据中心/VPS IP 验证 Grok Web 订阅时收到的 403 看起来像无效 cookie,导致用户去追查一个实际上正常的 cookie。非认证 403(Cloudflare 挑战 / 反机器人响应体)现返回消息说明 cookie 可能正常,拦截是基于 IP 信誉的——请从住宅 IP 重试或配置代理(认证类型的 403 保留重新粘贴指引)。 ([#3474](https://github.com/diegosouzapw/OmniRoute/issues/3474) — 感谢 @friedtofu1608) - **fix(db): 使大量待处理迁移安全阈值可通过环境变量覆盖** — 从旧版本恢复备份数据库可能触发"检测到 N 个待处理迁移 … 阈值为 50",且无法覆盖硬编码的 `50`。该阈值现可通过 `OMNIROUTE_MAX_PENDING_MIGRATIONS` 配置(启动时解析;`0` 禁用检查)。 ([#3416](https://github.com/diegosouzapw/OmniRoute/issues/3416) — 感谢 @samuraiIT) - **test(proxy): 覆盖 Vercel 中继 `proxyFetch` 路径** — 为 `buildVercelRelayHeaders` 和 `vercel` 类型中继短路(`x-中继-target`/`-path`/`-认证`、TCP 跳过、缺失认证 fail-closed)新增测试,关闭 [#2743](https://github.com/diegosouzapw/OmniRoute/issues/2743) 中跟踪的延期测试缺口之一。(感谢 @diegosouzapw) - **fix(cli): 在原生模块错误消息中提示 `omniroute runtime repair`** — Node 主版本升级后,`better-sqlite3` 的预编译二进制与 ABI 不匹配,服务可能崩溃循环;错误仅提示 `npm rebuild better-sqlite3`(对全局/无工具链安装无效)。启动和 SQLite 错误提示现也指向已有的自修复命令 `omniroute runtime repair`(重建到用户可写的运行时),并添加了顶层 `omniroute repair` 别名。 ([#3476](https://github.com/diegosouzapw/OmniRoute/issues/3476) — 感谢 @Rahulsharma0810) - **fix(antigravity): 每请求 Pro 系列上游 ID 容灾链(`gemini-3.1-pro-high` 400)** — Antigravity 静默重命名了 Gemini 3.1 Pro-high 上游 ID,导致 `gemini-3.1-pro-high` 开始返回 HTTP 400(而 `-low` 仍然正常),且实时 ID 无法静态确定(竞争对手代理意见不一)。执行器现对 400 重试备用 ID(`gemini-3.1-pro-high` → `gemini-pro-agent` → `gemini-3-pro-high`,pro-low 同理),有限且仅在 400 时触发,正常路径零额外开销;1:1 层级直通不变量得以保留(链是请求时的,而非静态别名映射)。 ([#3786](https://github.com/diegosouzapw/OmniRoute/issues/3786) — 感谢 @aliaksandrsen) - **fix(sse): 对单模型请求的早期流关闭(`STREAM_EARLY_EOF`)重试一次** — 不稳定的 OpenAI 兼容上游(例如 NVIDIA NIM 搭配 minimax-m3 / qwen3.5 / glm-5.1)间歇性发送 HTTP 200 然后以零有效帧关闭 SSE,表现为 502 "Stream ended before producing useful content"。此前只有 Antigravity 获得了早期关闭重试;所有其他服务商在非 Combo 单模型路径上立即返回 502。现通过有限的一次重试(仅早期关闭——非就绪超时——且不标记账户不可用)进行泛化。(同一报告中独立的 qwen-web 验证 SSRF 部分已在 v3.8.24 中修复,[#3767](https://github.com/diegosouzapw/OmniRoute/pull/3767)。) ([#3758](https://github.com/diegosouzapw/OmniRoute/issues/3758) — 感谢 @Svatosalav) - **fix(models): 在自动同步/导入中保留已隐藏模型** — 通过可见性(眼睛)开关隐藏模型以仅保留 Combo 模型的操作,在每次模型导入或自动同步时被撤销,所有模型重新显示。同步重新导入将"隐藏"等同于"删除"并丢弃两者;现通过独立的 `isDeleted` 标记将垃圾/删除路径(重新导入时仍丢弃,#3199)与眼睛开关(保留为已列出但隐藏)区分开来,且已隐藏模型不再在同步时重新别名为可路由目录。 ([#3782](https://github.com/diegosouzapw/OmniRoute/issues/3782) — 感谢 @xenstar) - **fix(providers): 修正 lmarena cookie 提示(`session` → `arena-auth-prod-v1`)** — lmarena 凭证提示要求名为 `会话` 的 cookie,但 lmarena.ai 的真实认证 cookie 是 `arena-认证-prod-v1`,因此仅粘贴 `会话=…` 的用户会遇到校验失败。凭证名称、占位符和存储密钥现已使用正确的名称(保留旧版 `会话` 密钥以向后兼容已保存的凭证)。 ([#3810](https://github.com/diegosouzapw/OmniRoute/issues/3810) — 感谢 @xspylol) - **fix(reasoning): 默认将 OpenAI 兼容的 `max` 强度标准化为 `xhigh`** — OpenAI 兼容服务商不接受字面量 `max`,但部分上游(例如通过 OpenRouter 的 DeepSeek)支持 `xhigh`;`max` 现映射为 `xhigh`,除非目标模型明确退出 `xhigh`,Claude 别名变体仍然遵循规范的 Claude 退出列表。 ([#3826](https://github.com/diegosouzapw/OmniRoute/pull/3826) — 感谢 @rdself) - **fix(combo): 在轮询流式路径上返回重放响应** — 带有流式传输目标的轮询 Combo 返回了一个已被就绪探测锁定的响应体,表现为 500 "ReadableStream is locked";轮询路径现像优先级路径一样返回重放克隆。 ([#3811](https://github.com/diegosouzapw/OmniRoute/pull/3811) — 感谢 @0xtbug) - **fix(claude): 从 Claude 模型 ID 中去除推理强度后缀** — 带有强度后缀的 Claude ID(`…-low` … `…-max`)在上游返回 404 并触发熔断器进入误导性的"速率受限"状态;后缀现已在分发前去除。 ([#3807](https://github.com/diegosouzapw/OmniRoute/pull/3807) — 感谢 @zhiru) - **fix(sse): 及时刷新已路由的 SSE 块(ping/僵尸就绪过滤器)** — Combo 流就绪现过滤 ping/僵尸帧,使已路由的 SSE 块无需等待就绪窗口即可流出。 ([#3759](https://github.com/diegosouzapw/OmniRoute/pull/3759) — 感谢 @rdself) - **fix(models): 不要自动隐藏 Test All 中的瞬时(速率限制/超时)失败** — 跨多个模型的并行 Test All 可能对账户造成速率限制,并自动隐藏每个 429/超时的模型(将其从 `/v1/模型` 中移除);瞬时失败现显示错误状态但保持可见。 ([#3849](https://github.com/diegosouzapw/OmniRoute/pull/3849) — 感谢 @lukmanc405) - **fix(quota): 将 OpenCode Go 缺失的配额 API 显示为锁定诊断** — 配额端点返回 404/401 的 OpenCode Go 密钥不再反复请求已失效的端点;该缺口被锁定并附带清晰消息和 `OMNIROUTE_OPENCODE_GO_QUOTA_URL` 覆盖提示。 ([#3838](https://github.com/diegosouzapw/OmniRoute/pull/3838) — 感谢 @adivekar-utexas) - **fix(pricing): 添加缺失的 Kiro 模型定价行** — 注册表提供的 Kiro 模型(例如 `claude-sonnet-4.6`)没有定价行并报告 $0.00;已添加相应行。 ([#3835](https://github.com/diegosouzapw/OmniRoute/pull/3835) — 感谢 @artickc) - **fix(ui): 通过 flagcdn SVG 渲染国旗以兼容 Windows** — Windows 不渲染区域指示符国旗 emoji;国旗现使用 flagcdn SVG 并附带 emoji 回退。 ([#3814](https://github.com/diegosouzapw/OmniRoute/pull/3814) — 感谢 @rafacpti23) - **fix(ui): 使用垂直调整手柄扩展请求日志表** — 请求日志表现在显示约 10 行,并可垂直调整大小。 ([#3820](https://github.com/diegosouzapw/OmniRoute/pull/3820) — 感谢 @rafacpti23) - **fix(i18n): 翻译 37 个语言环境中缺失的 `embeddedServices` 键** — `embeddedServices` 字符串在 37 个语言环境中显示为 `__MISSING__`;现已翻译。 ([#3819](https://github.com/diegosouzapw/OmniRoute/pull/3819) — 感谢 @rafacpti23) ### 🔒 安全与加固 - **fix(security): CCR 跨租户 IDOR — 按主体划分的作用域存储 + 有界内存** — 压缩 CCR 作用域存储此前在所有主体之间共享,允许跨租户读取;现已按主体划分作用域并设置内存边界。 ([#3859](https://github.com/diegosouzapw/OmniRoute/pull/3859)) - **feat(supply-chain): 构建来源、SBOM、Trivy 扫描 & OpenSSF Scorecard(建议性)** — 添加了 npm 构建来源、CycloneDX SBOM、Trivy 镜像扫描和 OpenSSF Scorecard 工作流(Quality Gates Fase 8 · Bloco A,建议性)。 ([#3824](https://github.com/diegosouzapw/OmniRoute/pull/3824)) ### 🧹 内部 / 质量 / 文档 - **将邮箱隐私控制合并到设置 → 外观** — 每个页面的邮箱隐私开关被替换为单一的全局开关。 ([#3822](https://github.com/diegosouzapw/OmniRoute/pull/3822) — 感谢 @rdself) - **docs(ui): 澄清路由设置文案(策略同步 + 粘性限制)** — ([#3843](https://github.com/diegosouzapw/OmniRoute/pull/3843) — 感谢 @adivekar-utexas) - **Quality Gates — Fase 7 & 8** — 将死代码 / 认知复杂度 / 类型覆盖率门槛提升为阻塞项,安装建议性 CI 扫描器(gitleaks / osv / actionlint / zizmor),并添加属性 + 黄金 + SSE 正确性测试以及运行时韧性(混沌 / 堆增长 / k6 压力)套件。 ([#3809](https://github.com/diegosouzapw/OmniRoute/pull/3809), [#3858](https://github.com/diegosouzapw/OmniRoute/pull/3858), [#3808](https://github.com/diegosouzapw/OmniRoute/pull/3808), [#3854](https://github.com/diegosouzapw/OmniRoute/pull/3854)) - **fix(docs): 为 `SUPPLY_CHAIN.md` 添加 MDX frontmatter** — 新的安全文档缺少 MDX 页面所需的 `title:` frontmatter,导致生产构建和 Docker Hub 发布失败;已添加 frontmatter。 ([#3864](https://github.com/diegosouzapw/OmniRoute/pull/3864)) - **chore(deps): 升级 `aquasecurity/trivy-action` 0.28.0 → 0.36.0** ([#3862](https://github.com/diegosouzapw/OmniRoute/pull/3862)) - **chore(quality): 为 Prettier 膨胀的 v3.8.25 修复 + `chat.ts` 增长调整文件大小基准** — 重新冻结每文件大小基准,以吸收本周期聊天核心和 Combo 修复带来的格式化/行数增长(手动编辑,绝不自动上调)。 ([#3823](https://github.com/diegosouzapw/OmniRoute/pull/3823), [#3833](https://github.com/diegosouzapw/OmniRoute/pull/3833) — 感谢 @diegosouzapw) - **test(suite): 发布时确保单元套件通过 — 将过期测试对齐到本周期的预期行为 + 修复两个新套件的波动性** — 发布门槛维护:更新了落后于预期行为变更的测试(OpenCode Go 锁定配额消息 #3838、邮箱隐私控制合并到设置 #3822、SOCKS5 默认开启代理类型消息、`[id]` 服务商详情绞杀榕分解 #3501、Vertex Express-mode 密钥、使用当前用户可调用模型 ID 的 Antigravity 发现)以及同服务商 503 穿透韧性测试;修复了压缩基准可重复性测试的波动性(连续通过)和 ServiceSupervisor 崩溃测试的波动性(轮询替代固定睡眠)。无生产代码变更。同时将 `OMNIROUTE_MAX_PENDING_MIGRATIONS`(#3416)记录到 `.env.example` + `ENVIRONMENT.md` 中。(感谢 @diegosouzapw) --- ## [3.8.24] — TBD _See English CHANGELOG for v3.8.24 details._ --- ## [3.8.22] — TBD _See English CHANGELOG for v3.8.22 details._ --- ## [3.8.21] — 2026-06-11 _See [English CHANGELOG](/CHANGELOG.md) for v3.8.21 details._ --- ## [3.8.20] — Unreleased _Development cycle in progress._ --- ## [3.8.19] — Unreleased _Development cycle in progress._ --- ## [3.8.18] — Unreleased _Development cycle in progress._ --- ## [3.8.17] — Unreleased _Development cycle in progress._ --- ## [3.8.16] — Unreleased _Development cycle in progress._ --- ## [3.8.15] — Unreleased _Development cycle in progress._ --- ## [3.8.14] — Unreleased _Development cycle in progress._ --- ## [3.8.13] — Unreleased _Development cycle in progress._ --- ## [3.8.12] — Unreleased _Development cycle in progress._ --- ## [3.8.11] — Unreleased _Development cycle in progress._ --- ## [3.8.10] — Unreleased --- ## [3.8.9] — Unreleased --- ## [3.8.8] — 2026-06-01 ### 新增 - **插件框架** (`src/lib/plugins/`, `/api/plugins/*`, `/dashboard/plugins`) — 钩子 + 注册表统一,插件 SDK (`definePlugin`),工作线程沙箱,每插件钩子速率限制,SHA-256 完整性校验,语义化版本升级管控,执行分析。插件路由仅限本地回环 (`isLocalOnlyPath`),且 `child_process` exec 需通过 `OMNIROUTE_PLUGINS_ALLOW_EXEC` 显式启用。(#2913 / #3041 — 感谢 @oyi77) - **插件系统: 响应钩子接线 + 启动加载 + 示例插件** — 将插件 `onResponse` 钩子接入聊天成功路径,在服务器启动时加载活跃插件使其在重启后存活(`pluginManager.loadAll()` 在 `server-init` 中),附带 `welcome-banner` 示例插件 (`examples/plugins/`) 以及全面的插件测试套件。(#3045 — 感谢 @oyi77) - **API 密钥选项: 禁用非公开模型** — 每密钥标志,限制密钥只能访问已发现、公开的模型(combos / `auto/*` / `qtSd/*` 路由仍允许)。(#3017 — 感谢 @androw) - **会话池 — 模块化且��服务商无关** (`open-sse/services/sessionPool/`) — 池化 cookie/会话管理器,支持轮询指纹轮换(每个池化会话有不同指纹), 每会话冷却/退避,与服务商无关的 `webExecutorWrapper`。为 DuckDuckGo Web 和 LLM7 服务商添加了池化支持,以及 MCP `poolTools` 工具集。(#2954 / #2978 — 感谢 @oyi77) - **AgentBridge** (`/dashboard/tools/agent-bridge`) — MITM(中间人代理),整合 9 个 IDE 智能体 (Antigravity, Kiro, GitHub Copilot, OpenAI Codex, Cursor IDE, Zed Industries, Claude Code, Open Code, Trae 桩),提供服务卡片、每智能体设置向导、模型映射表、 绕过列表、上游 CA 证书支持,并从旧版 `/dashboard/system/mitm-proxy` 重定向。 详见 `docs/frameworks/AGENTBRIDGE.md`。(#2858 — 感谢 @diegosouzapw) - **流量检查器** (`/dashboard/tools/traffic-inspector`) — LLM 感知的 HTTPS 调试器,支持 4 种捕获模式(AgentBridge 钩子、自定义 Hosts DNS、HTTP_PROXY :8080、系统级代理)、 DevTools 分屏界面、7 个详情标签页(对话、请求头、请求、响应、耗时、LLM 详情、 统计)、可调整大小的面板、会话录制(.har/.jsonl 导出)、SSE 流合并器、 对话规范化器(跨服务商)、系统提示指纹着色及注释。 详见 `docs/frameworks/TRAFFIC_INSPECTOR.md`。 - **MITM 处理器基类 + 9 个智能体处理器** (`src/mitm/handlers/`) — `MitmHandlerBase` 抽象 类,包含 `hookBufferStart`/`hookBufferUpdate` 用于流量检查器集成;以及全部 9 个智能体的具体处理器。 - **MITM 目标注册表** (`src/mitm/targets/`) — 每个智能体的声明式 `MitmTarget` 形态; 生成 `DATA_DIR/mitm/targets.json` 用于动态 `server.cjs` 解析。 - **流量检查器核心** (`src/mitm/inspector/`) — `TrafficBuffer` 内存环形缓冲区、 `kindDetector`、`sseMerger`(从 chouzz/llm-interceptor MIT 移植)、`conversationNormalizer` (MIT 移植)、`contextKey` 指纹识别、`httpProxyServer`、`systemProxyConfig`。 - **AgentBridge 透传 + 绕过** (`src/mitm/passthrough.ts`) — 为非映射主机提供 TCP 隧道; 绕过列表包含默认敏感主机模式 + 用户自定义模式。 - **上游 CA 证书** (`src/mitm/upstreamTrust.ts`) — `AGENTBRIDGE_UPSTREAM_CA_CERT` 用于 企业 TLS 环境。 - **密钥脱敏** (`src/mitm/maskSecrets.ts`) — 在任何日志或流量检查器广播前,对 sk-/Bearer/通用令牌进行脱敏。 - **数据库迁移 073–075** — `agent_bridge_state`, `agent_bridge_mappings`, `agent_bridge_bypass`, `inspector_custom_hosts`, `inspector_sessions`, `inspector_session_requests`. - **约 28 个 API 路由** 位于 `/api/tools/agent-bridge/`(12 个路由)和 `/api/tools/traffic-inspector/`(16+ 个路由)。全部为 LOCAL_ONLY + SPAWN_CAPABLE。 - **国际化**:`agentBridge.*` 和 `trafficInspector.*` 命名空间下所有新增键的 PT-BR + EN 翻译; 所有其他语言自动回退到 EN。 - **端到端冒烟测试** — `tests/e2e/agent-bridge.spec.ts`, `tests/e2e/traffic-inspector.spec.ts`, `tests/e2e/agent-bridge-traffic-cross.spec.ts` (在 CI 上由 `RUN_AGENT_BRIDGE_E2E` / `RUN_TRAFFIC_INSPECTOR_E2E` / `RUN_CROSS_E2E` 跳过控制)。 - **文档** — `docs/frameworks/AGENTBRIDGE.md` 和 `docs/frameworks/TRAFFIC_INSPECTOR.md`; 更新了 `docs/architecture/REPOSITORY_MAP.md`;更新了 `docs/reference/openapi.yaml`, 新增约 28 个路由和 20+ 个 Schema。 - **国际化:** 翻译乌克兰语(uk-UA)菜单和界面字符串,并完成完整的 uk-UA 界面覆盖 (#2981 / #2988 — 感谢 @Lion-killer) - **服务商:** 添加 SiliconFlow 端点选择器 (#2975 — 感谢 @xz-dev) - **OAuth:** 添加 Trae SOLO 服务商(work/code 模式)(#2964 — 感谢 @S0yora) - **服务商:** 添加 Qwen Web(chat.qwen.ai)web-cookie 服务商 (#2947 — 感谢 @oyi77) - **配额共享引擎 — 跨服务商配额池** — 监控/费用重构,外加配额共享引擎:分组选择器、分组池卡片、独占配额 API 密钥 (`allowedQuotas`)、通过 combos 的 `quotaShared-*` 路由模型、三步池向导(旧版 Plans 页面已退役)、端点 + 密钥预览以及完整的池编辑功能。新增配额池数据库迁移。(#2859 / #3022 / #3032 — 感谢 @diegosouzapw) - **仪表盘页面重新设计(导航重构)** — 智能体技能 + omni 技能,带动态 42 技能目录和 MCP/A2A 发现 (#2827);CLI Code + CLI Agents + ACP Agents 页面 (#2839);翻译器友好重新设计,5 个标签页 → 2 个 (#2847);功能性 `/batch` + `/batch/files` 重新设计 (#2849);Playground Studio + Search Tools Studio (#2869);记忆引擎重新设计 — sqlite-vec + 混合 RRF + Studio 界面 (#2873)。(感谢 @diegosouzapw) - **notion:** 添加 Notion 作为 MCP 上下文源 — 6 个工具 (`notion_search`、`notion_list_databases`、`notion_get_database`、`notion_query_database`、`notion_read`、`notion_append_blocks`),作用域为 `read:notion` / `write:notion`,配有仪表盘"上下文源"标签页、设置 API 和 `key_value` 表中的令牌持久化 (#2959) ### 变更 - 侧边栏工具组:在 `cloud-agents` 之后添加了 `agent-bridge` 和 `traffic-inspector` 项。 - `/api/tools/agent-bridge/` 和 `/api/tools/traffic-inspector/` 已添加到 `src/server/authz/routeGuard.ts` 中的 `LOCAL_ONLY_API_PREFIXES` 和 `SPAWN_CAPABLE_PREFIXES`。 - `.env.example`:记录了 9 个新的环境变量(`AGENTBRIDGE_UPSTREAM_CA_CERT`、 `INSPECTOR_BUFFER_SIZE`、`INSPECTOR_HTTP_PROXY_PORT`、`INSPECTOR_HTTP_PROXY_AUTOSTART`、 `INSPECTOR_TLS_INTERCEPT`、`INSPECTOR_SYSTEM_PROXY_GUARD_MINUTES`、`INSPECTOR_MAX_BODY_KB`、 `INSPECTOR_MASK_SECRETS`、`INSPECTOR_LLM_HOSTS_EXTRA`、`INSPECTOR_INTERNAL_INGEST_TOKEN`)。 ### 已修复 - **codex/服务商:** `POST /api/providers/[id]/refresh`(手动/自动"刷新令牌"端点)不再轮换使用轮换刷新令牌的服务商(Codex/OpenAI 共享一个 Auth0 `client_id`)。这是最后一个未设防的主动刷新入口点: 当仪表盘在页面加载时自动刷新每个即将过期的连接(或旧缓存前端批量调用该端点), 每个 Codex 账户的一次性 refresh_token 都会被轮换,Auth0 吊销整个令牌族 (`openai/codex#9648`) — 除最后一个外所有账户都会因 `[403] ` 而失效。该端点现在对轮换服务商跳过主动轮换,并交由响应式、串行化的 401 路径处理(与 `refreshAndUpdateCredentials` 和连接测试路由使用相同的守卫)。 - **codex/配额:** 打开配额 / 服务商仪表盘不再断开 Codex 多账户设置。配额同步路径 (`refreshAndUpdateCredentials`) 曾主动刷新每个连接 — 对于 轮换刷新令牌的服务商(Codex/OpenAI 共享一个 Auth0 `client_id`),它 并发刷新了兄弟账户,导致 Auth0 吊销整个令牌族 (`openai/codex#9648`),除最后一个外所有账户都因 `[403] ` 而失效。配额路径现在对轮换服务商跳过主动刷新 (`rotationGroupFor`),并复用当前的 access_token, 将真正的过期交由响应式、串行化的 401 路径处理。纵深防御: `serializeRefresh` 现在在两个 _排队_ 的兄弟刷新之间留有冷却间隔 (默认 2000 毫秒,可通过 `CODEX_REFRESH_SPACING_MS` 调整,设为 `"0"` 可 退出),同时立即释放单个刷新,因此响应式路径不会增加延迟。 - **负载规则:** 保存的负载规则现在在服务器重启后依然有效。当没有 内存中的覆盖设置时(启动钩子运行前的新进程,或 独立构建中的单独模块实例),`getPayloadRulesConfig` 现在在读取文件配置之前先读取数据库持久化的规则(真实数据源), 而不是静默返回空的文件默认值。(#2986) - **模型/自定义:** 自定义模型现在可以携带每个模型的 `targetFormat` 覆盖(例如,一个 opencode-go 自定义模型必须使用 Anthropic Messages 格式)。此前自定义模型始终按 OpenAI 兼容格式路由,因为 `targetFormat` 既未持久化,也未在路由时查询。已贯穿 `addCustomModel`/`replaceCustomModels`/`updateCustomModel`、API Schema/路由、`getModelInfo` 以及 chatCore 的 targetFormat 解析。(#2905) - **服务商/pollinations:** 路由至 `gen.pollinations.ai/v1` 而非已 废弃的 `text.pollinations.ai` 主机,后者现在对所有模型返回 `404 "legacy API"`。 gen 网关是当前的 OpenAI 兼容端点。(#2987) - **执行器/codex:** 对免费计划 Codex 账户 (`workspacePlanType === "free"`) 移除 CLI 注入的 `image_generation` 托管工具, 这些账户无法在服务端运行该工具,否则会收到上游 400 错误。付费计划保留该工具。 (镜像 CLIProxyAPI 的免费计划守卫;从 #2980 分析中分离出来) - **仪表盘:** 自定义服务商(`openai-compatible-*` / `anthropic-compatible-*`) 现在在活跃请求面板、代理日志和首页服务商拓扑中显示用户指定的节点名称,而非原始 UUID id。 显示标签解析器已提取为共享工具,供所有界面复用 (此前只有请求日志查看器解析了该标签)。(#2968) - **docker:** 独立启动器(Docker `CMD`)现在遵循 `OMNIROUTE_MEMORY_MB`(默认 512,范围 [64, 16384])并覆盖 镜像 `NODE_OPTIONS` 回退值,修复了高负载 / 大型 SQLite 数据库下的随机 OOM 崩溃。 - **docker:** 添加 `web` compose 配置文件(`omniroute-web`,目标 `runner-web`, 镜像 `omniroute:web`),使 web-cookie 服务商(gemini-web、claude-web、 claude-turnstile)开箱即用 — 默认 `base` 镜像不包含 Chromium/Playwright,导致这些服务商失败并报错 "Executable doesn't exist at .../ms-playwright/chromium..."。(#2832) - **路由/codex:** 修复两个 gpt-5.5 Codex 缺陷 (#2877)。(A) 对于仅有 Codex 的账户,裸 `gpt-5.5` Responses 请求被重新路由到 codex,但模型 被硬编码为 `gpt-5.5-medium`(`chatHelpers.ts`);执行器将该 `-medium` 后缀解读为显式 `modelEffort`,根据 #2331 覆盖了 客户端的 `reasoning.effort=xhigh`,静默降级 — 现在保留裸 `gpt-5.5` id,使客户端 effort 生效。(B) `gpt-5.5-xhigh`/`-high`/`-low` 被错误路���到 `openai`(→ 对仅有 codex 的用户显示"No credentials");带后缀的 变体现在已加入 `CODEX_PREFERRED_UNPREFIXED_MODELS`,使其推断为 codex。 - **sse/chatCore:** 移除 `handleChatCore` 中重复的 `const settings` 声明 (随每密钥流默认模式功能一同引入)。同一作用域内的重复声明导致 esbuild/tsx 失败, 报错"The symbol 'settings' has already been declared",使所有 导入 chatCore 的单元测试变红并破坏了生产构建。现在复用之前 合并的 `settings` 常量。 - **db/数据库迁移:** 解决 `077` 迁移版本冲突 (`077_api_key_stream_default_mode.sql` vs `077_quota_pools.sql`),该冲突导致 `getMigrationFiles()` 抛出异常并在启动时阻塞 `getDbInstance()`(应用无法 启动;所有涉及数据库的测试均失败)。将无依赖、 幂等的 `quota_pools` 迁移重新编号为 `085`,将非幂等的 `api_key_stream_default_mode` `ALTER` 保留在 `077`,添加了回溯性的 `isSchemaAlreadyApplied` 守卫(case `085`),以及一个强制唯一迁移前缀的回归测试。 - **路由/推理重放:** OpenCode `big-pickle`(服务商 `opencode`/`oc` 和 `opencode-zen`)现在通过新的 `RegistryModel.interleavedField` 字段声明交错 `reasoning_content` 契约, 使后续/工具使用轮次能够重放 reasoning_content。此前 `big-pickle` 未匹配任何重放模式, 失败并报错 `[400] The reasoning_content in the thinking mode must be passed back to the API`(其 DeepSeek-thinking 上游无法从模型 id 检测到, 且 `requiresReasoningReplay` 不消费 `supportsReasoning`)。 `getResolvedModelCapabilities` 现在会暴露注册表中的 `interleavedField`。(#2900) - **服务商/github-copilot:** 内置的 GitHub Copilot Claude Opus 和 Gemini 模型(`claude-opus-4.7`、`claude-opus-4-5-20251101`、`gemini-3.1-pro-preview`、 `gemini-3-flash-preview`)不再携带 `targetFormat: "openai-responses"`,因此 它们通过 `chat/completions` 路由(服务商默认值,与正常工作的 `claude-opus-4.6` 相同),而非 Responses API,因为 Copilot 不为 非 OpenAI 模型提供 Responses API(会返回 `[400]`)。原生 OpenAI `gpt-*` 模型保留 Responses API。(#2911) - **翻译器/responses:** Codex Desktop 将 `image_generation` 托管 工具注入到每个 Responses API 请求中(即使是纯文本请求),OmniRoute 曾拒绝该请求并报错 `[400] image_generation tool type is not supported`。现在 将其视为 `tool_search`:允许通过工具类型校验器,并在转发到 Chat Completions 之前 从工具数组中静默移除。(#2950) - **combo/构建器:** 无需认证的 OpenCode Free combo 条目现在使用 `oc/` 路由 别名,而非 `opencode/` 前缀。`parseModel("opencode/")` 解析到 `opencode-zen` API 密钥层级(通过手动 `ALIAS_TO_PROVIDER_ID` 覆盖),因此使用裸服务商 id 构建的 combo 会被错误路由,偏离 无需认证的 `opencode` 服务商;`oc/` 解析正确。(#2901) - **容灾/服务商:** 路由限制 `403`(例如 Fireworks Fire Pass `fpk_*` 密钥在 `/models` 上返回"…not authorized for this route.",而 聊天仍然正常)不再将连接标记为不可用。服务商 校验对于此类 403 会回退到聊天探测,而不是返回 "Invalid API key",且 `checkFallbackError` 将它们短路为无冷却。 真正的认证失败(401 / 通用 403)仍然快速失败。(#2929) - **认证/opencode-zen:** OpenCode Zen 免费模型现在可以在 Playground 和 combo 中无需 API 密钥即可使用。`opencode-zen` 提供公开、无需注册的 端点 (`https://opencode.ai/zen/v1`);当未配置 API 密钥连接时, 凭证解析现在回退到匿名(无需认证)访问, 而不是失败并报错"No credentials for provider: opencode-zen"。配置的 活跃密钥在存在时仍然会被使用。(#2962) - **翻译器/responses:** 修复了上游 `[400] Messages with role 'tool' must be a response to a preceding message with 'tool_calls'` 错误,当 Codex 客户端发送带有空/缺失 `call_id` 的 `function_call` 时触发。孤立的 `function_call_output` 此前绕过了孤立过滤器。现在 空 `call_id` 函数调用会被跳过(不产生悬空的助手 tool_call), 且任何没有匹配 tool_call id 的工具结果都会被丢弃。(#2893) - **deps:** 移除 `proxifly` npm 依赖 (#3000 — 感谢 @terence71-glitch) - **proxy:** OAuth 刷新时使用连接代理 (#3012 — 感谢 @terence71-glitch) - **usage:** 导出纯辅助函数供单元测试使用 (#3015 — 感谢 @oyi77) - **docs/docker:** 将内存默认文档对齐至 1024MB (#3006 — 感谢 @terence71-glitch) - **providers:** 修复 DuckDuckGo 缺少 API key 并更新 OpenCode 免费模型列表 (#3008 — 感谢 @NekoMonci12) - **claude:** 将 Claude Code 身份升级到 2.1.158 并同步 beta 标志 (#3010 — 感谢 @Tentoxa) - **test:** 提升 DB 和 usage 工具覆盖��达 60% 以上 (#3018 — 感谢 @oyi77) - **oom:** 解决 Bottleneck 限流器缓存和服务商注册表中的内存泄漏 (#2965 — 感谢 @soyelmismo) - **proxy:** 自定义代理流程迁移后在控制台显示注册表服务商代理 (#2963 — 感谢 @terence71-glitch) - **routing:** 将 agy 添加到 executor 映射以使用 AntigravityExecutor (#2957 — 感谢 @ReqX) - **skills:** 避免 Claude 助手 tool_result 阻塞 (#2956 — 感谢 @terence71-glitch) - **perf:** Bottleneck 限流器累积导致 CPU 泄漏及每请求优化 (#2951 — 感谢 @soyelmismo) - **combo:** combo 凭证解析忽略 target.providerId — 优先使用 Combo 目标的 providerId 而非模型推断的服务商 (#2946 — 感谢 @oyi77) - **dashboard:** v3.8.8 界面修复 — agent-bridge SSR + 审计/日志/记忆/工作室 (#2944) - **claude:** 对原生 Claude OAuth 清洗工具 schema 并隐藏第三方工具名称 (#2943 — 感谢 @NomenAK) - **auth:** 防止 Codex 多账户 refresh_token 家族吊销 (#2941) - **combo:** 修复 combo vision 透传和 Codex 工具历史修复 (#2940 — 感谢 @charithharshana) - **claude:** 将 WebSearch 映射�� Responses web_search (#2938 — 感谢 @makcimbx) - **claude:** 去除空的 Read pages 工具输入 (#2937 — 感谢 @makcimbx) - **dashboard:** 改进自助服务商配额可见性 (#2931 — 感谢 @guanbear) - **antigravity:** 避免可见的无签名工具历史 (#2927 — 感谢 @dhaern) - **sse/web-search:** Claude → Claude 透传时绕过 web-search 容灾,使原生 Claude 请求不被重写 (#2960 — 感谢 @terence71-glitch) - **oom:** 防止每请求内存累积(约 256MB 堆增长)(#2973 — 感谢 @soyelmismo) - **perf/proxy:** 并行化服务商代理覆盖查找 (#2984 — 感谢 @terence71-glitch) - **privacy/PII:** 正确解析 PII 特性标志并修复流式 SSE 请求中的 PII 响应清洗 (#3021 — 感谢 @dangeReis) - **electron:** 改进 macOS 窗口外观 (#3029 — 感谢 @bobbyunknown) - **i18n:** 修复缺失的 API key 作用域翻译 (#3031 — 感谢 @guanbear) - **stream/responses:** 对 Responses-API 客户端丢弃泄漏的聊天启动块 (#3035 — 感谢 @CitrusIce) - **docker:** `/app/data` 权限检查仅警告而非 `exit 1`,使不可写挂载不再导致容器启动失败 (#3036 — 感谢 @wussh) - **mcp:** 解决 streamable-HTTP 传输就绪状态报告为离线的问题 (#3037 — 感谢 @Chewji9875) - **dashboard:** 使用轻量级 ping 端点作为维护横幅(修复 #3040)(#3043 — 感谢 @herjarsa) - **test:** 解决已有测试失败 — env 同步、PII、配额、侧边栏 (#3039 — 感谢 @oyi77) - **docs/mcp:** 为 43 个工具重新生成 mcp-tools 图表并修复工具计数 (#3028 — 感谢 @diegosouzapw) - **mcp:** 将 `enforceScopes` 守卫移到 `MCP_TOOL_MAP` 查找之前,为 `withScopeEnforcement()` 添加内联 `scopes` 参数,并为所有 24 个动态工具定义(memory、skills、plugins、gamification、compression)声明作用域,修复动态 MCP 工具组的作用域执行 (#2958) --- ## [3.8.7] — 2026-05-29 ### ✨ 新功能 - **api (自助服务):** 新增 `GET /api/v1/me/status`,使委托 API key 可查看自身用量(已用 USD、预算百分比、token 总数)及可选的共享 Codex 账户配额,由迁移 `075_api_key_self_service_usage_scopes` 支持 (#2908 — 感谢 @guanbear)。 - **analytics:** 在原始日志清理前将用量日志汇总到 `daily_usage_summary`,并通过 SQL `UNION` 查询原始和汇总数据,防止分析历史数据丢失 (#2904 — 感谢 @unitythemaker)。 - **perf (内存):** 通过限制 11 个内存缓存、限制 SQLite 页面缓存、通过 Proxy 延迟加载服务商注册表以及优化 Next.js 启动时的数据库探测来降低服务器内存占用 (#2903 — 感谢 @soyelmismo)。 ### 🔧 问题修复 - **token-accounting:** 对于 Anthropic Claude 流式传输,优先使用 `prompt_tokens` 而非兼容的 `input_tokens`,避免重复计算缓存 token (#2904 — 感谢 @unitythemaker)。 - **agy:** 将 **Antigravity CLI (`agy`)** 作为独立的 OAuth 服务商,与 `gemini-cli`/`antigravity` 并列。它复用 antigravity 推理后端(相同的 Google client、`daily-cloudcode-pa.googleapis.com`),但提��自己的模型目录——特别是后端暴露的 Claude 模型(`claude-opus-4-6-thinking`、`claude-sonnet-4-6`)——以及自己的账户池和连接方法:导入 `agy` CLI token 文件(粘贴/上传)、��动检测本地 CLI 登录(`~/.gemini/antigravity-cli/antigravity-oauth-token`)、浏览器 OAuth 和批量/ZIP 导入。新路由:`POST /api/服务商/agy-认证/{import,import-bulk,zip-extract,apply-local}`。 ### 破坏性变更 - **proxy-logs:** `GET /api/usage/proxy-logs` 现在对每条日志返回 `clientIp` 而非 `publicIp`。读取 `log.publicIp` 的外部消费者必须更新为 `log.clientIp`。底层 SQLite 列(`public_ip`)保持不变,因此直接查询数据库的调用者不受影响 (#2880 — 感谢 @rdself)。 ### 已知不一致 - **log-export:** `GET /api/logs/export?type=proxy-logs` 返回原始 SQLite 行,其 IP 字段仍名为 `public_ip`(历史列名)。这与 `GET /api/usage/proxy-logs` 暴露的 `clientIp` 字段不同。这两个端点目前故意不一致,将在未来的迁移中对齐 (#2880)。 ### ✨ 新功能 - **usage:** 新增按 API key 的 token 限制,可按模型/服务商/全局设定,支持两级内联执行和内存缓存加速 (#2888 — 感谢 @mugnimaestra)。 - **providers:** 审查 web cookie 服务商,修复 4 个缺失的注册表条目,并新增 DuckDuckGo AI Chat 服务商 (#2862 — 感谢 @oyi77)。 - **compression:** 扩展 pt-BR 语言包,新增 34 条受 troglodita 项目启发的规则 (#2818 — 感谢 @leninejunior)。 ### 🔧 问题修复 - **oauth:** 紧急修复 Windsurf 登录——移除已失效的 PKCE 流程、提升 import-token 方式,并解决 SQLite 绑定类型错误 (#2884 — 感谢 @yunaamelia)。 - **models:** 清理非活跃连接中过期的同步可用模型,并安全地动态映射 Antigravity MITM 别名,避免循环引用 (#2886 — 感谢 @herjarsa)。 - **antigravity:** 通过将文本表示设为惰性来加固无签名的工具历史重放 (#2878 — 感谢 @dhaern)。 - **i18n:** 补全 144 个缺失的葡萄牙语 (pt-BR) 语言包 key 并与英语同步 (#2870 — 感谢 @alltomatos)。 - **opencode-go:** 为 OpenCode Go 添加服务商限制配额获取器,以检索 Z.AI 配额窗口 (#2861 — 感谢 @RajvardhanPatil07)。 - **reasoning:** 基于模型的交错能力元数据控制推理追踪重放注入 (#2843 — 感谢 @nickwizard)。 - **audio:** 为转录 form-data 手动构建 multipart body,防止 Next.js fetch 下丢失边界头 (#2842 — 感谢 @soyelmismo)。 - **gemini-cli:** 在模型同步期间优先使用真实的 Google Cloud 项目 ID 而非 default-project (#2841 — 感谢 @nickwizard)。 - **mcp:** 在 stdio MCP 模式下将启动时的 console.log 和 console.warn 消息重定向到 stderr,防止 JSON-RPC 解析失败 (#2840 — 感谢 @disonjer)。 - **antigravity:** 规范化未转义的 tool call 并将资源耗尽 429 错误归类为锁定冷却 (#2828 — 感谢 @Ardem2025)。 - **sse:** 修复 RTK 引擎默认值,解决连续行去重和直接压缩调用的问题 (#2825 — 感谢 @leninejunior)。 - **fix(usage):** 添加 opencode-go / opencode / opencode-zen 配额获取器,使服务商限制页面能显示 $12/5h、$30/wk、$60/mo 时间窗口及其他配额感知型服务商 ([#2852](https://github.com/diegosouzapw/OmniRoute/issues/2852) — 感谢 @apoapostolov) --- ## [3.8.6] — 2026-05-27 ### 🧹 维护 - **gitignore:** 忽略 `.claude/settings.local.json`,防止每个用户的 Claude Code 权限被意外提交 - **release:** 版本号提升和元数据同步 (package.json, package-lock.json, electron, open-sse, openapi.yaml) > v3.8.6 是一个维护/脚手架补丁。v3.8.5 之后周期的所有功能和问题修复工作(44 次提交——社区 PR #2777、#2782–#2787、#2789、#2790 以及内部紧急修复)已全部合并到 v3.8.5 中,并在该版本章节中记录。 --- ## [3.8.5] — 2026-05-26 ### 🔒 安全 - **authz:** 未认证时将 `/home` 和 `/home/:path*` 重定向到 `/login` (#2712) ### 🔧 问题修复 - **mcp:** 打破 callLogs ↔ compliance 的 ESM 循环依赖,该循环导致打包后的 MCP 服务器在 Node.js 24 上死锁 (#2650) - **deepseek:** 在 Node 严格模式下保护 PoW 求解器 Web Worker 处理器 (#2724) - **combos:** 在 combo 构建器选择器中包含免认证服务商 (#2737) - **translator:** 在 Responses API 翻译器中允许 `web_search` 服务端工具族 (#2695) - **oauth:** 注册缺失的 `trae` 服务商,使用 `import_token` 流程 (#2658) - **model:** 将基于设置的别名与旧版 DB 别名命名空间合并 (#2618, #2208) - **kiro:** HTTP / 非安全上下文的剪贴板回退 (#2689) - **cli:** 将 `omniroute serve` 就绪超时提升到 60s,并为 Windows 冷启动添加 TCP 回退 (#2460) --- ## [Unreleased] --- ## [3.8.23] — TBD --- ### ✨ 新功能 ### 🔧 问题修复 --- ## [3.8.4] — 2026-05-25 ### 新增 - Embedded services (work in progress — 9Router, CLIProxyAPI; see T-15 for full entry). --- ## [3.8.3] — 2026-05-24 ### ✨ 新功能 - **feat(combos):** 通用上下文交接,实现跨模型对话连续性 — 结构化 XML 摘要系统 (``),在 Combo 路由切换模型时保持对话连续性并处理状态转移。([#2653](https://github.com/diegosouzapw/OmniRoute/pull/2653) — 感谢 @herjarsa) - **feat(docs):** 将 `/docs` 迁移到 Fumadocs MDX,支持嵌套路由 — 用 Fumadocs 替换自定义文档引擎,新增 `[...slug]` 通配路由、`/docs/api/搜索` 搜索 API、`source.配置.ts` 内容配置,以及 8 个文档分区的 `meta.json` 导航文件(`architecture/`、`压缩/`、`frameworks/`、`guides/`、`ops/`、`reference/`、`路由/`、`security/`)。包含 50+ 个 URL 重定向以确保向后兼容。([#2614](https://github.com/diegosouzapw/OmniRoute/pull/2614) — 感谢 @ovehbe) - **feat(dashboard):** 为 `/dashboard/api-manager` 添加搜索和过滤功能 — 过滤栏支持按名称/key 搜索、仅活跃开关(持久化到 localStorage)、状态过滤(活跃/禁用/封禁/过期)、类型过滤(标准/管理/受限)、过滤计数徽章,以及带 "清除过滤" 按钮的空状态。([#2628](https://github.com/diegosouzapw/OmniRoute/pull/2628) / [#2641](https://github.com/diegosouzapw/OmniRoute/pull/2641) — 感谢 @diegosouzapw) - **feat(dashboard):** `/dashboard/providers` 中的免费层分组及符号链接 — 使用 `hasFree: true` 属性动态分组显示所有类别的免费层服务商,不将其从原生列表中移除。显示类别圆点和琥珀色圆点及可本地化提示,按服务商 ID 去重搜索结果,修正免费层计数统计。([#2632](https://github.com/diegosouzapw/OmniRoute/pull/2632) — 感谢 @diegosouzapw) - **feat(dashboard):** 敏感服务商风险提示弹窗 — 首次连接到基于会话或 OAuth 的服务商(如 Claude、Cursor、Copilot)时显示温和的信息警告弹窗。为 20 个服务商添加 `subscriptionRisk` 属性、可本地化模板,并将确认信息存储到 localStorage。([#2633](https://github.com/diegosouzapw/OmniRoute/pull/2633) / [#2638](https://github.com/diegosouzapw/OmniRoute/pull/2638) — 感谢 @diegosouzapw) - **feat(dashboard):** 重构免费层服务商仪表盘布局 — 清理视觉杂乱、重新组织类别、隐藏冗余横幅,将免费层类别优雅集成到主服务商界面。([#2640](https://github.com/diegosouzapw/OmniRoute/pull/2640) — 感谢 @diegosouzapw) - **feat(dashboard):** 内联迷你工作室(第四阶段)— 在服务商详情页集成交互式迷你工作室功能,包括专业示例卡片(Embedding、Image、LLM Chat、Music、STT、TTS、Video、Web Fetch、Web Search)、统一的 API Key 加载钩子、模型列表钩子和 curl 命令构建器。([#2648](https://github.com/diegosouzapw/OmniRoute/pull/2648) — 感谢 @diegosouzapw) - **feat(webfetch):** 分类支持,含专用媒体服务商页面和 Firecrawl、Jina Reader、Tavily 的执行器。([#2645](https://github.com/diegosouzapw/OmniRoute/pull/2645) — 感谢 @diegosouzapw) - **feat(adapta):** 集成 Adapta Org (`adapta-web`) 服务商,支持自动 Clerk 认证刷新和自定义引导教程弹窗。([#2643](https://github.com/diegosouzapw/OmniRoute/pull/2643) — 感谢 @df4p) - **feat(i18n):** 完成简体中文翻译 — 翻译 1220 个缺失 key,UI 覆盖率达 98.8%,零占位符。([#2655](https://github.com/diegosouzapw/OmniRoute/pull/2655) — 感谢 @L-aros) ### 🔧 问题修复 - **fix(settings):** Require Login modal Cancel button text and dismissal — modal now renders localized cancel label via the `common` namespace and closes correctly without modifying settings when cancelled. ([#2649](https://github.com/diegosouzapw/OmniRoute/pull/2649) — 感谢 @Chewji9875) - **fix(deepseek-web):** re-apply SSE parser, prompt format, and error handling fixes — handles all 3 DeepSeek SSE 流 formats (initial fragments, APPEND operations, bare string tokens), uses non-greedy regex for markdown image stripping, simplifies 提示 to single-turn, checks `json.code` before token extraction, and uses `accessToken` 容灾 for 会话 缓存 eviction on 认证 errors. ([#2616](https://github.com/diegosouzapw/OmniRoute/pull/2616) — 感谢 @ovehbe) - **fix(deepseek-web):** SSE thinking/search routing and session lifecycle — properly routes thinking vs content fragments based on `thinking_enabled` 标志, handles 搜索 results with citation indices, appends 搜索 result footnotes, refactors `transformSSE()` and `collectSSEContent()` with shared helpers. ([#2624](https://github.com/diegosouzapw/OmniRoute/pull/2624) — 感谢 @ovehbe) - **fix(codex):** use allowlist to strip non-Responses-API fields in non-passthrough path — strips residual Chat Completions fields (`stream_options`, `service_tier`, `store`, `metadata`) from the 请求 body when 路由 through the non-passthrough (translation) code path, preventing GPT-5.5 from receiving invalid parameters. ([#2615](https://github.com/diegosouzapw/OmniRoute/pull/2615) — 感谢 @diegosouzapw) - **fix(catalog):** skip static PROVIDER_MODELS when synced models exist — prevents stale/duplicate 模型 entries in `/v1/模型` for auto-synced 服务商. ([#2625](https://github.com/diegosouzapw/OmniRoute/pull/2625) — 感谢 @herjarsa) - **fix(qoder):** Cosy auth fallback for PAT tokens + vision support for qwen3-vl-plus — when a PAT token gets 401, falls back to Cosy 认证 against `api1.qoder.sh`; adds `supportsVision: true` to qwen3-vl-plus. ([#2629](https://github.com/diegosouzapw/OmniRoute/pull/2629) — 感谢 @herjarsa) - **fix(cli):** register tsx loader and add opencode config subcommand — registers `tsx/esm` at CLI startup so dynamic `.ts` imports resolve; adds `omniroute 配置 opencode` convenience alias. ([#2631](https://github.com/diegosouzapw/OmniRoute/pull/2631) — 感谢 @amogus22877769) - **fix(claude):** improve Pi and OpenCode compatibility — adds Pi Coding Agent anchors to system transform removal, stores `_toolNameMap` as non-enumerable, strips `context_management` when thinking is disabled. ([#2621](https://github.com/diegosouzapw/OmniRoute/pull/2621) — 感谢 @unitythemaker) - **fix(passthrough):** restore semantic passthrough system-role-only extraction — reverts full `normalizeClaudeUpstreamMessages()` to lighter `extractSystemRoleMessages()` in CC semantic passthrough paths, preventing document/tool chain corruption. ([#2620](https://github.com/diegosouzapw/OmniRoute/pull/2620) — 感谢 @Tentoxa) - **fix(kiro):** stabilize conversationId across prompt compression — captures pre-压缩 body and uses the original first 用户消息 as seed for UUID v5, keeping Kiro's AWS conversation context stable. ([#2630](https://github.com/diegosouzapw/OmniRoute/pull/2630) — 感谢 @HALDRO) - **fix(t3-chat-web):** close implementation gaps for t3.chat TanStack Start, tracking of stream_options, and retry configurations — parses TSS Turbo Stream Serialization from `_serverFn/*`, tracks 请求 `combo_strategy` via 数据库 迁移 `062_usage_history_combo_strategy.sql`, and makes batch 重试 backoffs custom-configurable via environment variables. ([#2634](https://github.com/diegosouzapw/OmniRoute/pull/2634) — 感谢 @oyi77) - **fix(reasoning):** extend empty `reasoning_content` injection to prevent tool call loops in Kimi K2 and replay models — injects empty `reasoning_content` 字段 to Kimi 模型 during tool-calling sequences to bypass loop issues. ([#2639](https://github.com/diegosouzapw/OmniRoute/pull/2639) — 感谢 @herjarsa) - **fix(cli):** Linux autostart via systemd user service on headless VPS — adds auto-generating systemd user service unit for headless setups on Linux, updating tray configs and system variables allowlist (`LOGNAME` and `XDG_CURRENT_DESKTOP`). ([#2635](https://github.com/diegosouzapw/OmniRoute/pull/2635) — 感谢 @janeza2) - **fix(combo):** preserve `` tag in SSE stream output for combos when using `context_cache_protection` to ensure correct context pinning round-trips. ([#2646](https://github.com/diegosouzapw/OmniRoute/pull/2646) — 感谢 @herjarsa) - **fix(rtk):** prevent false positives in RTK compression by skipping content-based filter matching for non-shell tool results (e.g. read_file, grep_search). ([#2642](https://github.com/diegosouzapw/OmniRoute/pull/2642) — 感谢 @HALDRO) - **fix(translator):** enable Claude extended thinking for Copilot Responses-API requests — handles reasoning 预算 and translations for Copilot. ([#2647](https://github.com/diegosouzapw/OmniRoute/pull/2647) — 感谢 @ivan-mezentsev) - **fix(tests):** remove duplicate assertion in schema coercion & fix(cli): ignore system vars in env check. (thanks @diegosouzapw) ### 📝 维护 - **chore(config):** ignore additional agent workflow command files (`.agents/commands/`). (thanks @diegosouzapw) - **chore(config):** ignore `memory-bank` and Cursor agent rules from tracking. (thanks @ovehbe) --- ## [3.8.2] — 2026-05-22 ### ✨ 新功能 - **feat(@omniroute/opencode-plugin):** 模型显示名称中添加上游服务商后缀 — 在丰富名称后追加服务商标签(如 `Claude Opus 4.7 · Claude` vs `Claude Opus 4.7 · Kiro`),使 OC TUI 模型选择器能区分通过不同上游连接路由的同 ID 模型。默认开启,可通过 `features.providerTag: false` 关闭。([#2602](https://github.com/diegosouzapw/OmniRoute/pull/2602) — 感谢 @mrmm) - **feat(@omniroute/opencode-plugin):** 服务商标签改为前缀 + 交通灯压缩表情 — 服务商标签现在前置显示(`Claude - Claude Opus 4.7`)以改善 TUI 列分组,长标签智能缩写(`GitHub Models` → `GHM`)。压缩管线以表情渲染强度(🟢🟡🟠🔴)。([#2604](https://github.com/diegosouzapw/OmniRoute/pull/2604) — 感谢 @mrmm) - **feat(providers):** 新增 7 个免费层服务商(第一波)— Arcee AI、InclusionAI、Krutrim、Liquid AI、MonsterAPI、Nomic 和 Poolside 现已作为新的 API-key 服务商提供,包含服务商图标、模型规格和完整路由支持。([#2479](https://github.com/diegosouzapw/OmniRoute/pull/2479) — 感谢 @oyi77) - **feat(providers):** 新增 Astraflow 服务商支持,含全球 + 中国端点 — 新服务商提供双区域基础 URL,支持全球和中国大陆访问。([#2486](https://github.com/diegosouzapw/OmniRoute/pull/2486) — 感谢 @ucloudnb666) - **feat(providers):** 新增 `claude-web` 服务商 — 基于 Cookie 的 Claude Web 聊天访问,无需 OAuth。([#2476](https://github.com/diegosouzapw/OmniRoute/pull/2476) — 感谢 @oyi77) - **feat(providers):** 新增 14 个免费层服务商(第一波 b)— 360AI、百川、百度、字节/豆包、IDEO、快手/可灵、昆仑/Skywork、商汤/SenseNova、阶跃星辰、腾讯混元、智谱 GLM、Replicate、RunPod 和 Modal,包含服务商图标、模型规格和路由支持。([#2488](https://github.com/diegosouzapw/OmniRoute/pull/2488) — 感谢 @oyi77) - **feat(hermes):** 新增丰富的多角色 Hermes Agent CLI 支持 — 7 个可配置角色(default、delegation、vision、compression、web_extract、skills_hub、approval),每个角色可选择模型并生成 YAML 配置,仪表盘卡片带预览和首页小组件集成。([#2526](https://github.com/diegosouzapw/OmniRoute/pull/2526) — 感谢 @apoapostolov) - **feat(cloud-agents):** 云端 Agent UX 全面改版 — 标签页(tasks/agents/settings)、状态过滤器、Material 图标、时长格式化、云端 Agent 凭证和健康 API 端点、内存统计端点。([#2516](https://github.com/diegosouzapw/OmniRoute/pull/2516) — 感谢 @oyi77) - **feat(authz):** 具有 manage 作用域的 API key 可从非本地地址访问 `/api/mcp/*` — Route Guard Tiers 系统(LOCAL_ONLY / ALWAYS_PROTECTED / MANAGEMENT),为受 `manage` 作用域控制的远程 MCP 访问开辟窄通道;`/api/cli-tools/runtime/*` 保持严格本地限制。包含仪表盘 AuthzSection、清单 API 和完整文档。([#2473](https://github.com/diegosouzapw/OmniRoute/pull/2473) — 感谢 @mrmm) - **feat(home):** 为高级用户定制首页 — 将服务商配额固定到首页,通过外观设置开关快速入门和服务商拓扑的可见性。([#2531](https://github.com/diegosouzapw/OmniRoute/pull/2531) — 感谢 @apoapostolov) - **feat(home):** 服务商配额自动刷新 — 可配置间隔(60s–600s),在外观设置中可开关;自动刷新固定在首页的配额信息。([#2532](https://github.com/diegosouzapw/OmniRoute/pull/2532) — 感谢 @apoapostolov) - **feat(@omniroute/opencode-plugin):** OmniRoute OpenCode 插件 — 从 OmniRoute API 获取实时模型,Combo 感知的模型列表,Gemini 请求清洗,多实例支持,认证流程集成,以及 10 个测试文件。([#2529](https://github.com/diegosouzapw/OmniRoute/pull/2529) — 感谢 @mrmm) - **feat(executors):** 将 OpenCode 客户端头转发到上游服务商 — OpenCode 特定头现在通过 executor 管线转发以改善兼容性。([#2538](https://github.com/diegosouzapw/OmniRoute/pull/2538) — 感谢 @kang-heewon) - **feat(fireworks):** 新增模型并支持 `modelIdPrefix` — 通用注册表字段,存储短模型 ID 并在上游 API 调用前拼接完整路径前缀。新增 6 个 Fireworks 模型、`modelsUrl` 动态同步和 Qwen3 重排序器。([#2560](https://github.com/diegosouzapw/OmniRoute/pull/2560) — 感谢 @HALDRO) - **feat(@omniroute/opencode-plugin):** 可读 + 可过滤 + 离线韧性模型选择器 — `usableOnly` 过滤器(仅显示连接健康的服务商)、`diskCache` 离线水合、`Combo:` 前缀标签、压缩元数据标签。([#2572](https://github.com/diegosouzapw/OmniRoute/pull/2572) — 感谢 @mrmm) - **feat(smart-pipeline):** 自动 Combo 路由的多阶段管线 — 基于规则 + 意图分类器 + 领域特定阶段,含可配置管线路由器、准确率基准测试和全面测试。([#2551](https://github.com/diegosouzapw/OmniRoute/pull/2551) — 感谢 @oyi77) - **feat(ops):** 通过 `OMNIROUTE_SKIP_DB_HEALTHCHECK=1` 跳过启动时 DB 健康检查 — 将缓慢的 `integrity_check`(大 WAL 上需 7 分钟以上)替换为 `quick_check`,并添加环境变量以完全跳过。([#2554](https://github.com/diegosouzapw/OmniRoute/pull/2554) — 感谢 @soyelmismo) - **refactor(dashboard):** 服务商配额分组布局,含垂直侧栏 — 将页面重构为每个服务商 2 列布局(左侧栏含图标/名称/状态,右侧内容含动态按服务商列),新增 `providerColumns.ts` / `ProviderGroup.tsx` / `AccountRow.tsx` 组件,环境芯片过滤行,按组批量刷新和内联展开面板。([#2528](https://github.com/diegosouzapw/OmniRoute/pull/2528) — 感谢 @Gi99lin) - **feat(providers):** 新增注册表中缺失的 26 个免费层服务商 — Novita、Avian、Chutes、Kluster、Targon、Nineteen、Celery、Ditto、Atoma 等。([#2590](https://github.com/diegosouzapw/OmniRoute/pull/2590) — 感谢 @oyi77) - **feat(providers):** 新增 api-airforce 免费服务商,含 55 个模型。([#2587](https://github.com/diegosouzapw/OmniRoute/pull/2587) — 感谢 @oyi77) - **feat(dashboard):** 可配置侧边栏 — 预设、拖拽排序、智能分组,以及新的设置 → 侧边栏页面。([#2581](https://github.com/diegosouzapw/OmniRoute/pull/2581) — 感谢 @Gi99lin) ### 🔧 问题修复 - **fix(validation):** 当 Gemini 基础 URL 已以 `/models` 结尾时不再追加第二个 `/models` — 使用默认基础 URL 的 Google AI Studio 连接此前验证请求会访问 `.../v1beta/models/models` 并返回 `404`。([#2545](https://github.com/diegosouzapw/OmniRoute/issues/2545)) - **fix(cloudflare-ai):** 将 Workers AI (`cf/`) executor 的 OpenAI content-part 数组展平为纯字符串 — Workers AI 的 `/ai/v1/chat/completions` 会以 HTTP 400 拒绝 `content: [{type:"text",...}]`,现在文本部分会合并为字符串。([#2539](https://github.com/diegosouzapw/OmniRoute/issues/2539)) - **fix(i18n):** 将配额仪表盘中英文源文件里残留的葡萄牙语字符串替换为英文 — 配额分享 Beta 通知 (`betaConfigSaved*`) 和服务商配额行中 `Edit cutoffs` / `Refresh now` 的回退文本此前显示为葡萄牙语。([#2540](https://github.com/diegosouzapw/OmniRoute/issues/2540)) - **fix(proxy):** 在 `resolveProxyForProvider` 中遵循旧版按服务商/全局代理配置 — Claude OAuth Token 交换和 Token 刷新仅查询了新的代理注册表,因此以旧方式(`/api/settings/代理?level=服务商`)配置的代理被忽略,导致交换请求直接从主机发出,在 VPS 部署上触发 Anthropic 的 IP `rate_limit_error`。现在会回退到旧版配置,与 `resolveProxyForConnection` 行为一致。([#2456](https://github.com/diegosouzapw/OmniRoute/issues/2456)) - **fix(antigravity):** 在失败前通过 `loadCodeAssist` 自动发现缺失的 Cloud Code `projectId` — 新重新添加的 Antigravity 账户如果存储的 `projectId` 为空(OAuth 发现时未返回),现在会在首次请求时恢复项目信息,而非返回 `422 Missing Google projectId`,与 `gemini-cli` 启动流程一致。([#2334](https://github.com/diegosouzapw/OmniRoute/issues/2334), [#2541](https://github.com/diegosouzapw/OmniRoute/issues/2541)) - **fix(stream):** 为严格客户端保持 `/v1/responses` SSE 连接活跃 — 在上游生成首个 Token 前发送早期 keepalive,并将心跳间隔降至 4s,使 Codex CLI 的 `reqwest` 客户端(≈5s 空闲读取超时)不再在慢速/推理模型上丢弃流 "before 补全"。`curl` 不受影响,因为它没有空闲超时。([#2544](https://github.com/diegosouzapw/OmniRoute/issues/2544)) - **fix(electron):** 首次启动时延长服务器等待时间,并在响应后重新加载 — 长时间的上线后 DB 迁移可能超过 30s 就绪探测,导致桌面应用卡在 "Server starting" 屏幕。探测现在以宽松超时针对无需认证的健康端点,并在服务器就绪后重新加载窗口。([#2460](https://github.com/diegosouzapw/OmniRoute/issues/2460)) - **fix(cli):** mark `bin/omniroute.mjs` as executable (mode 755) so the globally-installed CLI runs directly without a manual `chmod +x`. ([#2469](https://github.com/diegosouzapw/OmniRoute/issues/2469) — 感谢 @disonjer) - **fix(settings):** 在服务器启动和 JSON/SQLite 导入后将全局 System Prompt 恢复到内存配置中 — 此前仅在 PUT 端点加载,因此任何重启或导入后开关/prompt 都会静默回退到默认值。([#2470](https://github.com/diegosouzapw/OmniRoute/issues/2470) — 感谢 @disonjer) - **fix(settings):** 将全局 System Prompt 追加到**现有系统内容之后**而非之前,使注入系统消息的服务商/代理指令(Kiro、OpenCode、Hermes 等)不再因近因效应覆盖用户的全局 prompt。([#2468](https://github.com/diegosouzapw/OmniRoute/issues/2468) — 感谢 @disonjer) - **fix(kiro):** 通过 Kiro social-auth 端点而非 AWS SSO OIDC 刷新已导入的社交 Token(`authMethod === "imported"`)— 已导入的 Token 携带已注册的 `clientId`/`clientSecret`,但刷新 Token 由社交渠道签发,OIDC 客户端无法刷新,因此自动刷新失败并提示 "provider returned no new token"。([#2467](https://github.com/diegosouzapw/OmniRoute/issues/2467) — 感谢 @disonjer) - **fix(antigravity):** 从 `providerSpecificData` 中解析 Cloud Code `projectId` 作为容灾回退(并在 Token 刷新期间保留),使将项目存储在该处的连接不再在 Gemini `/v1beta` 流式路径中返回虚假的 `422 Missing Google projectId` 错误。([#2480](https://github.com/diegosouzapw/OmniRoute/issues/2480)) - **fix(api):** `GET /v1beta/models` 现在仅列出其服务商拥有活跃/已验证连接的模型,与 OpenAI 格式的 `/v1/models` 行为一致,而非返回整个目录。([#2483](https://github.com/diegosouzapw/OmniRoute/issues/2483)) - **fix(cli):** 将 `STORAGE_ENCRYPTION_KEY` 持久化到 `DATA_DIR`(而非仅 `~/.omniroute`),并在 `storage.sqlite` 已存在时拒绝自动生成新密钥 — 新密钥无法解密先前加密的凭据,静默重新生成会导致用户无法访问数据库。CLI 现在镜像了服务器的 `bootstrapEnv` 守卫逻辑。(由 Daniel Nach 报告;原始密钥持久化由 @Chewji9875 实现 — 对 [#1622](https://github.com/diegosouzapw/OmniRoute/issues/1622) 的跟进修复) - **fix(gemini):** 在 Gemini 思考模型工具调用中保留并重新附加 `thoughtSignature` — 将签名命名空间贯穿 `FORMATS.GEMINI` 和 `FORMATS.GEMINI_CLI` 请求翻译器,使缓存的签名(按连接 + 工具调用 id 索引)在后续轮次中能被找到。修复了在 Gemini 代理工具使用中出现的 `[400]: Function call is missing a thought_signature in functionCall parts` 错误。([#2504](https://github.com/diegosouzapw/OmniRoute/issues/2504)) - **fix(translator):** 在 Gemini 路径中接受以 Responses-API `input_file` 格式发送的 PDF,在 Responses/Codex 路径中接受以 Gemini 风格 `document` 格式发送的 PDF — 内容部分现在在 `input_file` / `file` / `document` 之间统一规范化,因此 PDF 能到达模型,无论客户端使用哪个字段名。([#2515](https://github.com/diegosouzapw/OmniRoute/issues/2515)) - **fix(stream):** 将 `thinking` 数组和 `reasoning_details` 计为有效的流式输出 — 纯推理响应(如低 `max_tokens` 下的 Mistral/StepFun)此前被错误分类为 "Stream ended before producing useful content" 并变为虚假的 502 错误;现在已被识别为有效输出。([#2520](https://github.com/diegosouzapw/OmniRoute/issues/2520)) - **fix(claude):** 在 Claude Code 语义透传路径中提取 system/developer 角色消息 — 在发送给 Anthropic 之前,将 `role:"system"` / `role:"developer"` 消息从 `messages[]` 数组移动到顶层的 `system` 参数,因为 Anthropic 会拒绝消息数组内的此类消息。修复了记忆注入上下文被静默丢弃的问题。([#2497](https://github.com/diegosouzapw/OmniRoute/pull/2497) — 感谢 @unitythemaker) - **fix(vision-bridge):** 通过 OmniRoute self-loop 自动路由非标准服务商模型 — 视觉桥接现在能检测模型是否原生支持视觉,并自动通过 OmniRoute 自身端点重新路由图像进行格式翻译。([#2487](https://github.com/diegosouzapw/OmniRoute/pull/2487) — 感谢 @herjarsa) - **fix(mitm):** 添加 IPv6 DNS 重定向、模块化 Antigravity 目标、改进日志记录 — MITM DNS 处理器现在能正确处理 IPv6 (AAAA) 查询重定向(此前仅支持 IPv4),添加了专用的 `antigravity.ts` 目标模块,并增强了 DNS/TLS 日志以便调试。([#2514](https://github.com/diegosouzapw/OmniRoute/pull/2514) — 感谢 @herjarsa) - **fix(usage):** 改进 Claude 和 MiniMax 套餐标签检测 — 对 Claude OAuth 用量(tier/plan/subscription_type/org 字段)进行了更完善的套餐名称解析,并新增从配额总量推断 MiniMax 套餐标签的功能。([#2498](https://github.com/diegosouzapw/OmniRoute/pull/2498) — 感谢 @Gi99lin) - **fix(codex):** 并行扇出图像 `n` 请求 — 当 Codex 请求 `n > 1` 张图像时,图像生成处理器现在并发而非顺序派发请求,显著降低了总延迟。([#2499](https://github.com/diegosouzapw/OmniRoute/pull/2499) — 感谢 @nmime) - **fix(embeddings):** 从上游响应中剥离过时的 `Content-Encoding` 头 — 防止客户端收到声明为 `identity` 编码但实际为 gzip 编码的响应,这会导致静默数据损坏。([#2477](https://github.com/diegosouzapw/OmniRoute/pull/2477) — 感谢 @lordavadon2) - **fix(model):** 对未识别的模型返回明确错误而非静默回退到 OpenAI 默认 — 此前未识别的模型会静默回退到 OpenAI;现在返回 404 并附带描述性消息列出已知服务商。([#2492](https://github.com/diegosouzapw/OmniRoute/pull/2492) — 感谢 @herjarsa) - **fix(dark-mode):** 修正压缩覆盖选择器的背景 Token — Combo 压缩覆盖 `