Removed the visibility: "debug" field from the 'devtools' section in
sidebarVisibility/sections.ts. It was the only section using this gate,
so with debugMode defaulting to false, Playground/Translator/Search
Tools were completely absent from both the live Sidebar and Settings ->
Sidebar, with no toggle and no explanation.
2026-09-21 21:35:35 -03:00
1635 changed files with 106073 additions and 166016 deletions
@@ -100,7 +100,7 @@ Returns the value to place under `provider.omniroute` inside `opencode.json`.
| `baseURL` | `string` | Yes | OmniRoute base URL. Accepts `http://host:port`**or**`http://host:port/v1`. Trailing slashes are tolerated. |
| `apiKey` | `string` | Yes | OmniRoute API key. Use `sk_omniroute` for local installs that have `REQUIRE_API_KEY=false`. |
| `displayName` | `string` | No | Custom name shown in the OpenCode UI. Default: `"OmniRoute"`. |
| `models` | `string[]` | No | Override the surfaced model catalog. Default: 8 curated models — see `OMNIROUTE_DEFAULT_OPENCODE_MODELS`. |
| `models` | `string[]` | No | Override the surfaced model catalog. Default: 4 curated models — see `OMNIROUTE_DEFAULT_OPENCODE_MODELS`. |
| `modelLabels` | `Record<string,string>` | No | Human-readable labels keyed by model id. |
Throws on empty/invalid input — `baseURL` must be a real URL, `apiKey` must be a non-empty string.
@@ -143,7 +143,7 @@ Duplicates and empty strings are dropped automatically, and order is preserved.
- **Requests 404 with `/v1/v1/...`** — you're on an old version (≤1.0.0). Update to `≥0.1.0` of this re-released package. The new build normalises `baseURL` automatically.
- **`401 Invalid API key`** — your OmniRoute instance has `REQUIRE_API_KEY=true` but the key you supplied doesn't exist there. Create one via the dashboard or set `REQUIRE_API_KEY=false` and use `sk_omniroute`.
- **OpenCode complains the provider has no models** — supply an explicit `models` list; the default 8 may be hidden by your provider visibility settings.
- **OpenCode complains the provider has no models** — supply an explicit `models` list; the default 4 may be hidden by your provider visibility settings.
- **commit-msg**: `check:ai-attribution` — rejects AI/bot `Co-Authored-By` trailers and AI-generation footers in the message (Hard Rule #16; human co-authors allowed; also in the `quality.yml` fast-gates loop (PR→`release/**`) and a PR-only `ci.yml` lint step (PR→`main`) — #14436)
_Living section — reconciled 2026-09-21 from all cycle commits (`091589089c` → `06f1df9d77`, 1,356 non-merge commits). Bullets carry the merged PR and its author; direct pushes are listed with their commit hash. Regenerated at each `/generate-release` phase._
_Living section — reconciled 2026-09-15 from all cycle commits (`091589089c` → `c0f92ec98a`, 916 non-merge commits). Bullets carry the merged PR and its author; direct pushes are listed with their commit hash. Regenerated at each `/generate-release` phase._
### 📊 Release by the numbers
| | |
| --- | ---: |
| 👥 People who contributed | **242** |
| 📝 Commits in the cycle | **1,356** |
| 🔀 Pull requests referenced | **1,328** |
| 📋 Changelog entries | **1,393** |
| 🙌 Contributors credited in entries | **240** |
| 🤖 Automated dependency commits | 22 |
| 👥 People who contributed | **123** |
| 📝 Commits in the cycle | **916** |
| 🔀 Pull requests referenced | **892** |
| 📋 Changelog entries | **919** |
| 🙌 Contributors credited in entries | **121** |
| 🤖 Automated dependency commits | 18 |
**Entries by type**
| Type | Count |
| --- | ---: |
| 🐛 Fixes | 962 |
| ✨ Features | 185 |
| 🧹 Chore | 112 |
| 🧪 Tests | 50 |
| 📚 Docs | 49 |
| 🐛 Fixes | 592 |
| ✨ Features | 131 |
| 🧹 Chore | 93 |
| 📚 Docs | 42 |
| 🧪 Tests | 32 |
| ♻️ Refactor | 8 |
| 🏗️ Build | 7 |
| ⚡ Performance | 7 |
| ⚙️ CI | 5 |
| 🔒 Security | 3 |
| 📦 Dependencies | 3 |
| ⚙️ CI | 2 |
| ⏪ Reverts | 2 |
| 📦 Dependencies | 2 |
| 🔒 Security | 1 |
### 🏆 Top 25 contributors this cycle
_By commits in `091589089c..06f1df9d77`, author identities consolidated via `.mailmap` and the merged PR's GitHub login. Bots excluded._
_By commits in `091589089c..c0f92ec98a`, author identities consolidated via `.mailmap` and the merged PR's GitHub login. Bots excluded._
- **feat(docs):** every Markdown page under `docs/` is now mirrored in all 65 dashboard locales, not only the 22-page core set — 152 sources × 65 locales = 9,880 mirrors (6,208 new), with the 🌐 language bar of every mirror rewritten for the full locale list. The docs drift gate (`npm run i18n:check`, blocking in CI) derives its scope from the tree, so it now guards all 152 pages. Found and fixed by the run in `scripts/i18n/run-translation.mjs`: a markdown table or tight bullet list with no blank line inside it (PROVIDER_REFERENCE.md's 244-row table, FREE_TIERS.md's 71-item list) was sent as one 16–40 KB request that outlived the backend socket for verbose scripts (Greek, Amharic); oversized runs of table rows or list items are now cut at item boundaries and rejoined without a blank line, so no chunk exceeds 6 KB across the docs tree. 48 older mirrors whose tables had lost rows were retranslated with the fixed chunker. ([#14106](https://github.com/diegosouzapw/OmniRoute/pull/14106))
- **feat(usage):**`openai-compatible-*` connections can now report billing/quota in Provider Limits. The connection declares its own quota endpoint, auth mode and a dot-path mapping onto `UsageQuota` in `providerSpecificData.quotaEndpoint`, so no upstream-specific code is needed per service — a mapping that resolves nothing reports no quota rather than an exhausted-looking 0/0 ([#13616](https://github.com/diegosouzapw/OmniRoute/issues/13616)) ([#13673](https://github.com/diegosouzapw/OmniRoute/pull/13673)) — thanks @abhisheksharma2411
- **feat(sse):** track LLM Gateway DevPass quota — the `llmgateway` provider now reads its monthly plan-credit and weekly premium-model allowance from `GET /v1/key` and surfaces both windows in Dashboard › Limits and quota-aware preflight ([#12462](https://github.com/diegosouzapw/OmniRoute/pull/12462)). — thanks @PixmaNts
- **feat(providers):** register `gemini-3.8-flash` ([#12638](https://github.com/diegosouzapw/OmniRoute/issues/12638)) — Gemini 3.8 Flash (DeepMind 2026-09-02) with tool calling and vision support ([#12663](https://github.com/diegosouzapw/OmniRoute/pull/12663)) — thanks @toor11
- **feat(proxylogs):** proxy log columns and detail pane now show the registry proxy name instead of a bare `host:port` when several registry entries share the same gateway ([#12814](https://github.com/diegosouzapw/OmniRoute/pull/12814)) — thanks @tiangao88
- **compression:** add Hungarian Caveman language pack with Hungarian-specific rules, language detection, localized output instructions, and language-pack tests. (#12825 - thanks @botii16)
- **feat(sse):** parse/scrub DSML tool-call markers embedded in reasoning and recognize adaptive thinking on the response side — `dsmlToolCalls.ts` module + translator/stream/handler wiring ([#12905](https://github.com/diegosouzapw/OmniRoute/pull/12905)) — thanks @initguru
- **feat(codex):** safely discover compatible models by classifying upstream models before activation to keep hidden, unsupported, retired, or newer-client models out of the active catalog, exposing candidate diagnostics while persisting only active models, adding GPT-6 Astra fallback definitions, and bumping the tested Codex CLI version to 0.153.4 ([#12933](https://github.com/diegosouzapw/OmniRoute/pull/12933)) — thanks @TheDemonTuan
- **feat(api):**`POST /api/keys` accepts `expiresAt` (ISO datetime, nullable) with the same semantics as the key-update path, so automation can create an expiring key in one operation instead of create-then-update. Omitted/null preserves the current non-expiring behavior; enforcement reuses the existing expiry policy ([#12952](https://github.com/diegosouzapw/OmniRoute/pull/12952)) — thanks @caniko
- **feat(build):** add build:fast and start:fast to bypass standalone tracing ([#13021](https://github.com/diegosouzapw/OmniRoute/pull/13021)) — thanks @tuandinh0801
- **feat(sse):**`OMNIROUTE_DISABLE_CONVERSATION_TRACKING=1` turns off conversation-history collection for operators who do not use the dashboard's conversation view. `resolveConversationId()` returns an untracked result before it reads SQLite or parses message history, and the switch also covers client-supplied session IDs. Routing-session handling is unchanged, tracking stays on by default, and existing records are not deleted. One reporting install held 5.97 million turn records at about 4.26 GB ([#13150](https://github.com/diegosouzapw/OmniRoute/pull/13150)) — thanks @cryptiklemur
- **feat(providers):** Add `auto/kimi`, `auto/qwen`, `auto/deepseek`, `auto/gpt`, and the `auto/claude-haiku` fast variant to the built-in routing catalog, including bare `k3` models on Kimi coding and web backends (issue #13214). ([#13709](https://github.com/diegosouzapw/OmniRoute/pull/13709)) — thanks @keii-2596
- **feat(usage):** Claude OAuth usage now shows the separate weekly Fable limit next to the shared five-hour and weekly meters. Anthropic reports it as a `weekly_scoped` entry in `limits[]`, which OmniRoute ignored, so the pool was invisible. The provider-limits cache keeps `modelQuotas` and restores it on stale-data fallback. The Fable meter is display-only and does not affect routing, account selection, or cooldowns ([#13266](https://github.com/diegosouzapw/OmniRoute/pull/13266)) — thanks @cryptiklemur
- **feat(playground): copy an individual Compare column's response.** Each column in the Compare tab now has a copy button beside the remove button, reusing the existing `useCopyToClipboard` hook to copy that column's response text and show a checkmark while `disabled` on an empty response. (The independent-scrolling half of this PR was already fixed separately in #13532.) (#13317 — thanks @ventulus95)
- **feat(models):** add Gemini 3.8 Flash tiers to Antigravity and AGY catalogs ([#13318](https://github.com/diegosouzapw/OmniRoute/pull/13318)) — thanks @tuandinh0801, with credit to #12499 (@Abhishekchhetri020)
- **feat(reasoning):** adaptive reasoning effort (`auto`) — the gateway resolves the thinking budget per user turn from deterministic request-shape signals (stateless per-turn pin) instead of forwarding a literal `auto`, applied at the gateway pre-translation for any harness (Claude Code, Cursor, Codex, opencode, Hermes) whose request dispatches to an OpenAI Chat-Completions-shaped upstream (`targetFormat === FORMATS.OPENAI` — `reasoning_effort` is an OpenAI-shaped field, so a Claude- or Gemini-targeted request is unaffected). Opt in via `X-OmniRoute-Effort: auto` or a model's `defaultReasoningEffort: "auto"` (now a valid `ModelSpec` value); any explicit client reasoning field always wins ([#13448](https://github.com/diegosouzapw/OmniRoute/pull/13448)) — thanks @patrykkopycinski
- **feat(dashboard):** Add a dedicated, full-width API-key routing editor with explicit model/combo choices, searchable selectors and protection for unsaved rule drafts. ([#13555](https://github.com/diegosouzapw/OmniRoute/pull/13555)) — thanks @JxnLexn
- **feat(proxies):** proxy pools and opencode's per-account rotation stop re-serving a proxy that just failed (refused TCP probe, or a 429 through it) for a period that doubles on each repeat up to a cap, without writing any proxy status; with every candidate set aside the choice is unchanged. Opt-in via the `PROXY_SKIP_RECENTLY_FAILED` feature flag (default off: selection unchanged) ([#13578](https://github.com/diegosouzapw/OmniRoute/pull/13578)) — thanks @maxmad64bis
- **feat(proxy-logs):** proxy log rows keep the HTTP status the provider actually returned (`upstream_status`, null when no response arrived), so a throttled egress IP (429), a refused one (403) and a provider outage (500) are no longer the same "error" line, and a 429 generated locally is no longer mistaken for one from the provider ([#13580](https://github.com/diegosouzapw/OmniRoute/pull/13580)) — thanks @maxmad64bis
- **feat(proxies):** the proxy pool editor shows, for the last 24 h, how many distinct egress IPs actually served the pool's members, how many connections went through them and the most seen behind one IP, read from the proxy log through a separate route so it can never break the pool screen; opt-in with the `PROXY_POOL_EGRESS_OBSERVATION` feature flag (default off) ([#13581](https://github.com/diegosouzapw/OmniRoute/pull/13581)) — thanks @maxmad64bis
- **feat(sse): learn hard request caps stated in 429 bodies and pace under them.** Providers such as TokenRouter reject bursts with prose like `Maximum 5 requests within 1 minutes` and no rate-limit headers, so the limiter never learned the ceiling and kept racing into it; every 429 also tore the limiter down and rebuilt it with no pacing. `updateFromResponseBody` now parses that phrasing (and `N requests per minute`, `N requests per M seconds`, `N RPM`) into a per-window cap, applies it to the limiter as an empty reservoir that refills `N` every window with calls spread `window / N` apart, and records it in `learnedRateLimits`. A learned cap is reapplied whenever the limiter is rebuilt after a 429 and when limits are restored at startup, unless the connection has an explicit RPM override. Fixes [#13594](https://github.com/diegosouzapw/OmniRoute/issues/13594). ([#13895](https://github.com/diegosouzapw/OmniRoute/pull/13895)) — thanks @costajohnt
- **feat(proxies):** a proxy pool stops re-serving a member the provider just refused through it and tries another member instead, reusing the existing skip cooldown; a later success through the member clears it. Opt-in with the `PROXY_SKIP_RECENTLY_FAILED` feature flag (default off: pool selection unchanged) ([#13602](https://github.com/diegosouzapw/OmniRoute/pull/13602)) — thanks @maxmad64bis
- **feat(flags):** add `DB_HEALTHCHECK_STARTUP_DEFERRED_ENABLED` (default off) — opt-in deferral of the startup DB health/integrity check past process boot via `setImmediate`; off keeps the pre-#13717 behavior of blocking `getDbInstance()` until the check has already run (#13717). — thanks @HouMinXi
- **feat(i18n):** 7 new locales — Hausa (`ha`), Yoruba (`yo`), Igbo (`ig`), Amharic (`am`), Uzbek (`uz`), Georgian (`ka`), Armenian (`hy`) — across the dashboard, docs mirrors, CLI, README and the site (66 locales, the full planned expansion from 43). (#13727)
- **feat(api):**`POST /v1/rerank` (and the memory engine's loopback rerank step) can route to OpenAI-compatible provider nodes on a LAN/Tailscale host — not only loopback — behind the new `RERANK_REMOTE_PROVIDER_NODES` feature flag (default off), subject to the provider outbound URL policy; the loopback host check is consolidated into `@/shared/network/loopbackNodeHost` shared by rerank, audio, and the local health checker ([#13732](https://github.com/diegosouzapw/OmniRoute/pull/13732)) ([#13733](https://github.com/diegosouzapw/OmniRoute/pull/13733)) — thanks @seanford
- **feat(security):** OmniRoute now warns at boot when the server that answers `/v1` inference is bound to a non-loopback interface while `REQUIRE_API_KEY` is disabled. The guard added in [#12568](https://github.com/diegosouzapw/OmniRoute/pull/12568) covered the API bridge (`API_HOST`, default loopback) and the live dashboard WebSocket, but not the Next server that actually serves `/v1/chat/completions` and `/v1/responses` — which binds `HOST || 0.0.0.0`, every interface by default. That matters because `GET /v1/models` follows the dashboard login posture (`requireAuthForModels`) while inference follows `REQUIRE_API_KEY`, so an instance with an admin password and `REQUIRE_API_KEY=false` answers `401` to the probe an operator naturally runs while inference stays open to anyone who can reach the port. The bound host is resolved from `OMNIROUTE_BOUND_HOST` (published by `scripts/dev/run-next.mjs`) then Next's own `HOSTNAME` (the Docker path); `HOST` is deliberately excluded because the standalone server ignores it and a warning naming the wrong interface is worse than none. New `docs/security/INFERENCE_AUTH_POSTURE.md` documents the split, how to actually probe inference, and the [#2257](https://github.com/diegosouzapw/OmniRoute/issues/2257) caveat that an invalid bearer degrades to anonymous. ([#13820](https://github.com/diegosouzapw/OmniRoute/pull/13820)) — thanks @abhisheksharma2411
- Expose combo wall-clock timeout (`comboTimeoutMs`) next to Target timeout in the combo editor and Combo defaults. Empty keeps the 10-minute hang-stop; a positive value replaces it. ([#13857](https://github.com/diegosouzapw/OmniRoute/pull/13857)) — thanks @HouMinXi
- **feat(i18n):**`retranslate-site` rewrites the site catalogs' verbatim-English leaves (2,059 across 63 catalogs; mean English residue 10.3 % → 6.3 %, the rest being brand names kept on purpose). (#13886)
- **feat(compression):** Lite tool-result truncation length is configurable (`lite.maxToolLength`, env `OMNIROUTE_LITE_MAX_TOOL_LENGTH`). Default stays 2000. An out-of-range step cap no longer hides a valid global cap; a toggle-only settings write keeps a stored cap; `maxToolLength: null` clears it. Dashboard copy no longer hard-codes 2,000 characters. ([#13915](https://github.com/diegosouzapw/OmniRoute/pull/13915) — refs [#13178](https://github.com/diegosouzapw/OmniRoute/issues/13178)) — thanks @HouMinXi
- **feat(proxy):** support multiple local core endpoints, one per line ([#13923](https://github.com/diegosouzapw/OmniRoute/pull/13923) — thanks @maxmad64bis)
- feat(providers): **Added Agnes AI (China) as `agnes-cn` pointed at `https://api.agnes-ai.cn/v1`. Keys issued for `apihub.agnes-ai.com` stay on the existing `agnes` card. Live `/v1/models` on that host lists `agnes-3.0-flash` (same id as intl); the CN seed matches 2.0/2.5/3.0 and not retired 1.5.** ([#13399](https://github.com/diegosouzapw/OmniRoute/pull/13399)) — thanks @HouMinXi
- **feat(api):** add per-key `allowAutoCombos` (default `true`) to gate the built-in `auto/*` combos, which previously bypassed a key's `allowedCombos`/`allowedModels`/`blockedModels` restrictions entirely — a restricted key could still reach any model through `auto/best-fast`. Also adds a per-key `catalogScope` (`all`/`combos`/`models`) to control what `/v1/models` advertises, and the dashboard gained an Auto Combos toggle and a catalog scope selector in the API key permissions UI. ([#13670](https://github.com/diegosouzapw/OmniRoute/pull/13670)) — thanks @fouadSalkini
- **feat(sse):** Claude OAuth connections can opt in (per account, Edit connection → Claude section) to Claude Code's lower-priority lane and once-a-week session-limit reset. After the first 5-hour usage-wall 429 carrying `anthropic-ratelimit-unified-slow-offer: treatment`, OmniRoute retries the same account with `anthropic-usage-limit: slow` and keeps sending it until the window resets — the account keeps serving past the limit instead of being cooled down (slot_busy/529 wait the server's `slow-retry-after`, bounded by `slow-max-wait`). With auto-reset on, the wall first tries `POST /api/organizations/{org}/reset_rate_limits` (`juniper_tide`) and retries at full speed when the server grants it. Both default off; nothing is sent before the limit is hit. ([#13074](https://github.com/diegosouzapw/OmniRoute/pull/13074)) — thanks @davidebaraldo
- feat(providers): update Fish Audio for S2.1 Pro Free, validated advanced TTS controls, and provider-scoped persistent voice-clone management. ([#13090](https://github.com/diegosouzapw/OmniRoute/pull/13090)) — thanks @Bl0ck154
- **feat(usage):** redeem **GLM Coding Plan Reset Cards** (`glm` / `glm-cn` / `glmt` / `zai`) from the Provider Limits UI — clear an exhausted 5-hour or weekly coding-plan window before it rolls over, via the new `/api/usage/glm-reset-card` route (`GET` lists, `POST` redeems). List and redeem requests egress through the connection's proxy and honor exclusive-lease isolation; z.ai's `requestId` is reused for retries of an ambiguous (transport-failed) redemption so a lost response cannot double-consume a card (in-memory, best-effort — restart the server and a fresh key is required). Responses are validated fail-closed (HTTP 200 alone is never treated as success), unavailable/expired cards are filtered and the list is sorted by earliest expiry, and the post-redemption quota refresh is best-effort: a refresh failure still reports the successful reset. ([#12754](https://github.com/diegosouzapw/OmniRoute/pull/12754)) — thanks @insoln
- **feat(routing): self-hosted unified OpenAI-compatible entry (`/v1/chat/completions`).** When `OMNIROUTE_SELF_HOSTED_PROVIDERS` (inline YAML) or `OMNIROUTE_SELF_HOSTED_PROVIDERS_FILE` is set, the existing `/v1/chat/completions` route diverts through the self-hosted provider adapters (`open-sse/services/providerAdapters.ts`) — OpenAI / Anthropic / local-compatible — instead of the cloud pipeline. Provider is auto-routed via the `x-omniroute-provider` header, a `provider/model` (or `provider::model`) model prefix, or the first configured provider; upstream credentials stay runtime-only and are stripped from echoed responses. Optional `OMNIROUTE_SELF_HOSTED_API_KEY` guards the entry with `Authorization: Bearer` (reserved for the D5 quota-key system); unset = open loopback/trusted-network route. Upstream failures return the standard OpenAI error shape (including a normalized 502 for unreachable providers). One OpenAI SDK snippet can now traverse multiple self-hosted providers without changing the client. (#RIC-738 / RIC-697 D4) ([#13611](https://github.com/diegosouzapw/OmniRoute/pull/13611)) — thanks @luyuehm
- **feat(routing): deterministic routing strategies for the self-hosted entry (`strategy:` block, M2/RIC-740).** The unified `/v1/chat/completions` entry (RIC-738) now accepts a declarative `strategy:` block — inline in the providers YAML or via `OMNIROUTE_SELF_HOSTED_STRATEGY` / `OMNIROUTE_SELF_HOSTED_STRATEGY_FILE` — expressing five explainable, non-predictive routing policies: blacklist / whitelist (hard filters), cooldown circuit breaker (`consecutiveFailures` + `cooldownMs`), cost-priority (cheapest `costPer1MInput` first), latency-aware (fastest recent average first), and an explicit `fallbackChain` order. The ordered candidate list is the fallback chain: a failed primary (network or non-2xx) falls through to the next candidate, and each failure feeds the breaker. Every response carries `x-omniroute-route-decision` — the one-line "why this model / why not that one" audit trail (D3). A pinned provider rejected by a hard filter returns `400` (never a silent re-route); no eligible providers returns `503` with the full explainable decision. No ML/predict dependency; malformed strategy config returns `500` rather than silently becoming a no-op. (#RIC-740 / RIC-697 D3) ([#13611](https://github.com/diegosouzapw/OmniRoute/pull/13611)) — thanks @luyuehm
- **feat(dashboard):** add sidebar pinned items shortcut section with individual item pin toggle and localStorage persistence ([#12891](https://github.com/diegosouzapw/OmniRoute/pull/12891)) — thanks @ZaimMarzuki
- **feat(providers):** share the existing `api.x.ai/v1/models` discovery config with `xai-oauth` so SuperGrok OAuth connections pick up new Grok ids without a registry seed edit. ([#13518](https://github.com/diegosouzapw/OmniRoute/pull/13518)) — thanks @HouMinXi
- **feat(dashboard):** show exact token counts on hover in usage analytics cards and tables ([#12553](https://github.com/diegosouzapw/OmniRoute/pull/12553)) — thanks @ZaimMarzuki
- **feat(i18n):** blocking key-completeness gate — every locale carries every en.json key ([#13827](https://github.com/diegosouzapw/OmniRoute/pull/13827))
- **feat(i18n):** new-key gate rejects __MISSING__ markers; skills translate new keys in parallel ([#13996](https://github.com/diegosouzapw/OmniRoute/pull/13996))
- **feat(mitm):** dynamically inject configured models into Antigravity model catalog ([#14006](https://github.com/diegosouzapw/OmniRoute/pull/14006)) — thanks @steve25060
- **feat(cache):** configurable dual-layer semantic caching with Redis/In-Memory vector stores (re-land of #12630) ([#14159](https://github.com/diegosouzapw/OmniRoute/pull/14159)) — thanks @BillyOutlast
### 🐛 Bug Fixes
@@ -948,372 +895,6 @@ _By commits in `091589089c..06f1df9d77`, author identities consolidated via `.ma
- **fix(guardrails):** skip credential redaction for base64 image data URLs ([#13550](https://github.com/diegosouzapw/OmniRoute/pull/13550)) — thanks @KooshaPari
- **fix(resilience):** increase requestQueue.maxWaitMs default from 15s to 30s ([#13553](https://github.com/diegosouzapw/OmniRoute/pull/13553)) — thanks @KooshaPari
- **fix(docs):** document OMNIROUTE_READY_TIMEOUT_MS and allowlist the test-only DISABLE_IOREG_STRATEGY ([#13692](https://github.com/diegosouzapw/OmniRoute/pull/13692))
- Electron release workflow: the `publish-npm` job now grants `actions: read` to the reusable `npm-publish.yml` it calls (its `publish` job requests it), which is what made GitHub refuse the whole v3.8.50 run at startup and ship the release with zero desktop assets; a `workflow_dispatch` now builds the requested tag instead of the dispatching branch and can skip the npm leg (`publish_npm=false`) when only re-attaching assets ([#11974](https://github.com/diegosouzapw/OmniRoute/pull/11974))
- **fix(api):**`/v1/files` and `/v1/batches` now enforce one ownership rule everywhere — a dashboard session is the instance operator, an API key acts on its own records only, and a record with no owner is denied to every non-session caller. Previously a file or batch whose `api_key_id` was null (a dashboard-session or anonymous upload, or a batch artifact inheriting one) could be read, downloaded, deleted, cancelled or used as a batch input by any other key or by an unauthenticated caller (GHSA-2jm2-mpx8-6523), and `GET /v1/files` / `GET /v1/batches` returned every tenant's records to an anonymous or invalid-bearer caller under the default `REQUIRE_API_KEY=false` (GHSA-m3hp-hq9g-fpmv) — both lists now fail closed with a `401`, and only a dashboard session without a key reads the whole instance. The same shared rule lets the dashboard cancel any batch, not just unowned ones. Behaviour change: the anonymous upload → batch → download flow no longer works without an API key, since a null owner cannot be attributed. Subsumes [#13683](https://github.com/diegosouzapw/OmniRoute/pull/13683) ([#13749](https://github.com/diegosouzapw/OmniRoute/pull/13749)) — thanks @hartmark
- **fix(i18n):** every dashboard catalog other than `pt-BR` (64 locales) went through the same quality review `pt-BR` received in #13885 — each leaf changed by the 2026-09 retranslation was checked against its English source by the translation backend and rewritten where the meaning, placeholders, register or product terminology were off: 73,586 corrections net (75,263 applied, 1,677 that had turned a real translation into the plain English term reverted so the real-translation ratio gate stays where it was). `scripts/i18n/review-locale.mjs` now survives an upstream hiccup (per-batch retries with backoff, skipped batches listed in `_artifacts/i18n-review/<code>.skipped.json`), checkpoints the catalog every 25 batches instead of writing only at the end, and writes leaves whose own key contains a dot (`compliance.eventTypes["apiKey.ban"]`) instead of crashing. ([#14078](https://github.com/diegosouzapw/OmniRoute/pull/14078))
- **fix(auth):** closed the JWT_SECRET bootstrap chain (GHSA-7pq4-8pvv-rx7r). The fresh-install bootstrap gate in `isAuthRequired()` now decides "loopback" from the trusted peer — the token-stamped real TCP peer the custom server writes, the pipeline's own locality verdict, or a real socket — and never from the client-controlled `Host` / `nextUrl.hostname` whenever a stamping server is in front (every supported runtime), so `Host: localhost` from a remote address no longer opens the window; the anonymous first-password write (`POST /api/settings/require-login`) is under the same loopback constraint instead of being open to every network peer, and `managementPolicy` hands its `peerContext` verdict down explicitly. `/api/settings/obsidian` (incl. `/webdav`, which mints reusable WebDAV Basic credentials for a caller-chosen root served before Next.js) joined `ALWAYS_PROTECTED_API_PATHS`, and `enableObsidianVaultSync()` refuses a vault that is, sits inside, or contains the data directory (realpath-resolved), so the WebDAV file service can no longer be pointed at `server.env` / `storage.sqlite` ([#13791](https://github.com/diegosouzapw/OmniRoute/pull/13791))
- **Combo routing:** a context-cache-pinned model that returns `401` now falls through to the normal combo fallback loop instead of terminating the request, allowing other eligible connections or providers to serve it. ([#12818](https://github.com/diegosouzapw/OmniRoute/pull/12818)) — thanks @keeltrace
- **fix(sse):** the Antigravity account picked for a request can now be reserved for that request's streaming lifecycle, so a concurrent retry or the credential handoff cannot re-pick an account already committed to an in-flight stream; a fully leased pool answers with a structured 503 `antigravity_pool_busy` carrying a bounded `Retry-After`. Opt-in behind the new `ANTIGRAVITY_ACCOUNT_LEASE_ENABLED` flag (default off) (#10011) ([#13929](https://github.com/diegosouzapw/OmniRoute/pull/13929)) — thanks @Ardem2025
- **fix(docker):** bump the Bun image to 1.4.0, enable Turbopack on Bun, and port the node image's build memory guards so the `-bun` container builds fit the 16 GB GitHub runner instead of dying with `cannot allocate memory` ([#11719](https://github.com/diegosouzapw/OmniRoute/pull/11719)). Both images now default `OMNIROUTE_BUILD_WORKERS` to `2` (1 page-data worker) against the measured ~4.5 GB per-process RSS budget (#7518/#11663). ([#13140](https://github.com/diegosouzapw/OmniRoute/pull/13140)) — thanks @ozeas
- **security(runtime):** fail closed on hostile thrown values and keep upstream text out of public error surfaces — the chat pipeline now reads rejection metadata through a safe accessor, sanitizes the message before it reaches call logs and console, and projects the failure-usage code onto the bounded public vocabulary; Perplexity's non-streaming quota/upstream error body sanitizes the upstream message and projects the provider-supplied error code; Arena (lmarena) maps every public failure onto a fixed vocabulary instead of echoing the upstream error; Notion's TLS transport failure sanitizes the transport error before it reaches the response body ([#11742](https://github.com/diegosouzapw/OmniRoute/pull/11742)).
- fix(resilience): only clear the combo-level LKGP pin when it names the target that actually failed, so an unrelated target skip under `auto`/`round-robin` no longer discards a valid pin for a healthy provider (#12235) — thanks @abhisheksharma2411
- **fix(sse):** 429 bodies phrased as `N API calls / month` (Cohere trial keys) now classify as `quota_exhausted` instead of a short transient `rate_limit`, so a spent monthly allowance is no longer retried every few seconds for the rest of the billing cycle ([#12252](https://github.com/diegosouzapw/OmniRoute/pull/12252)) — thanks @brick30llc-ctrl
- fix(cache): fold `response_format`/Responses-API `text.format` into the semantic cache signature so a `temp=0` request can no longer be served a stored response body with a different output schema (#12307) ([#12309](https://github.com/diegosouzapw/OmniRoute/pull/12309)) — thanks @amirrezakm
- fix(gemini): preserve response-schema nullability across union flattening so a model with nothing to say returns a valid null instead of the string `"null"` or a fabricated value (#12308) ([#12310](https://github.com/diegosouzapw/OmniRoute/pull/12310)) — thanks @amirrezakm
- **fix(combo):** a priority combo whose steps are different models on one Claude OAuth connection now falls through to the next step — a model-specific 404 or 5xx is scoped to the model instead of retiring the whole account, while a 429 stays account-wide ([#12334](https://github.com/diegosouzapw/OmniRoute/issues/12334)) ([#12340](https://github.com/diegosouzapw/OmniRoute/pull/12340)) — thanks @Kizuno18
- fix(api): restore the `name` field on non-streaming `/v1/responses``function_call` output items — a plain (non-namespace) tool call's identity restore was blindly applying the `_toolNameMap` alias-table fallback as a `{namespace, name}` object, silently blanking `name` to `undefined` (dropped entirely by JSON.stringify) and leaving Codex unable to dispatch the call, so it re-narrated its intent in a loop instead (#12370) ([#13824](https://github.com/diegosouzapw/OmniRoute/pull/13824))
- **fix(memory):** extracted facts and oversized extraction input are now truncated at a word or sentence boundary instead of at a hard character offset. `sanitizeMatch()` (500-char fact cap) and `capExtractionText()` (64KB extraction-input cap) previously sliced at the exact limit, which could cut a fact mid-word or mid-clause; both now back the cut index off within an 80-char lookback window, preferring sentence-ending punctuation (`. ! ?`), then a plain word boundary, and only falling back to the original hard cut when neither is found — the same pattern already used for `compressToolResults` (#8169) — thanks @LeMonBLOCK ([#12383](https://github.com/diegosouzapw/OmniRoute/pull/12383))
- **fix(chatCore):** stop `executeWithUpstreamStartTimeout` leaking its abortPromise listener onto the long-lived client/stream signal, and stop `mergeAbortSignals` leaking per-attempt abort listeners, so a later hedge cancellation or client disconnect cannot reject an orphaned promise and take the process down (`Error [AbortError]: hedge-cancelled`). The crash guard also absorbs combo abort reasons (`hedge-cancelled`, `combo-per-model-timeout`) and raw string disconnect reasons as a last-resort net ([#12406](https://github.com/diegosouzapw/OmniRoute/pull/12406) — thanks @Beexly)
- **fix(db):** give `conversation_turn_nodes` its own independent retention knob (`retention.conversationTurnNodes`, default 30 days — matching `callLogs` so upgrading changes nothing until an operator overrides it) instead of sharing `callLogs`, and sweep orphaned `agentic_conversations` after the nodes expire (#12453). ([#13344](https://github.com/diegosouzapw/OmniRoute/pull/13344)) — thanks @HouMinXi
- **fix(usage):** Render OpenRouter PAYG account credits as a metered quota when no per-key spending limit is set ([#12468](https://github.com/diegosouzapw/OmniRoute/pull/12468)) — thanks @killer30001000
- **fix(cli):**`omniroute serve` no longer reports "Server did not respond within 60s" for a server that is actually up: the readiness probe's per-attempt timeout now escalates (2s, 4s, 8s, 15s, clamped to the time left in the budget) instead of aborting every attempt at a fixed 2s, so a health route that needs more than 2s for its first response is observed rather than repeatedly torn down. The timeout diagnostic now also states whether the port was accepting connections. ([#12484](https://github.com/diegosouzapw/OmniRoute/pull/12484)) — thanks @dmlanday
- **fix(cli):**`omniroute serve` now checks whether the port is already owned before spawning anything, and reports the conflict with the owning PID plus the two ways out (`omniroute stop`, or `--port`). Previously it handed the conflict to the child process, which died with `EADDRINUSE` and was retried twice on the supervisor's restart budget, printing three identical raw Node stack traces without ever saying that another instance held the port. Because that happened after the pid files were written, the doomed second instance also de-registered the healthy running one, leaving `supervisor/.pid` pointing at the dead starter and `server/.pid` deleted. ([#12485](https://github.com/diegosouzapw/OmniRoute/pull/12485)) — thanks @dmlanday
- **fix(sse):** Codex WebSocket transport (including the app-server) no longer fails to load in the Next.js standalone Docker runtime — the wreq-js loader now resolves its module name dynamically instead of a literal Turbopack could rewrite to an unreachable build-time symlink (#12491) ([#13756](https://github.com/diegosouzapw/OmniRoute/pull/13756)) — thanks @marshalfevzi
- **fix(devin):** treat Devin CLI model ids as literal — never strip or synthesize effort suffixes ([#12492](https://github.com/diegosouzapw/OmniRoute/pull/12492) — thanks @Neuron-Mr-White)
- **fix(command-code):** floor a tiny caller-set `max_tokens` (e.g. `64`) to `MUSE_SPARK_MIN_OUTPUT_TOKENS = 512` for muse-spark ids, detected through the prefix-aware `MUSE_SPARK_PATTERN` so provider-prefixed forms (`meta/muse-spark-1.2-contributor`, `cmd/meta/muse-…`) are covered in both `buildOpenAiBody` (the `/provider/v1/chat/completions` path from #12130) and `buildCommandCodeCliBody` (the `/alpha/generate` fallback) — the hidden server-side reasoning phase can no longer consume the whole output budget and answer HTTP 200 with null content (`out=64, reasoning=61`), mirroring the #11214 mitigation already shipped for opencode-go; a caller that sent no budget is left without one and budgets at or above the floor pass through untouched ([#12497](https://github.com/diegosouzapw/OmniRoute/pull/12497)) — thanks @Stazyu
- Fix `keys regenerate`/`keys reveal` in the CLI to fall back to the dashboard `/api/keys` route when an ID from `keys list` does not exist in the registered-keys store, closing an ID-namespace drift between the two API key families. ([#12520](https://github.com/diegosouzapw/OmniRoute/pull/12520)) — thanks @Gaulnews
- **fix(cli):** Windows dashboard no longer reports Claude Code as `settings_found_binary_unresolved` when npm-global detection fails inside Electron. A failed `npm config get prefix` is no longer cached as permanent `""` (which deleted every npm-derived candidate for the process lifetime), Windows lookup PATH is enriched with npm-prefix / `%APPDATA%\npm` / nvm / `%ProgramFiles%\nodejs`, and stock Node MSI `.cmd` shims under Program Files remain an explicit safety net. Separate from the #7831`.ps1` / known-path fix for #7774. ([#12563](https://github.com/diegosouzapw/OmniRoute/issues/12563)) ([#12565](https://github.com/diegosouzapw/OmniRoute/pull/12565)) — thanks @drmikecrypto
- **fix(sse):** strip `temperature`/`top_p` on native Codex Responses passthrough so combo `codex-review` traffic no longer 400s with `Unsupported parameter: temperature` ([#12585](https://github.com/diegosouzapw/OmniRoute/pull/12585)) — thanks @fouadSalkini
- **fix(pricing):** saving model pricing from the dashboard no longer fails with a 400 / `[object Object]` — sync-written pricing fields round-trip through PATCH and validation errors surface actionable details ([#12629](https://github.com/diegosouzapw/OmniRoute/pull/12629)) — thanks @wofiporia
- **fix(chat):** requests with null/non-object entries in `messages[]` are now rejected with a clear 400 instead of crashing translators with an HTTP 500 ([#12643](https://github.com/diegosouzapw/OmniRoute/issues/12643)) ([#13755](https://github.com/diegosouzapw/OmniRoute/pull/13755)) — thanks @soroush5
- **fix(sse):** Claude-native context handoffs now land in Anthropic's top-level `system` parameter instead of a leading `role: "system"` message, and the final Claude executor dispatch hoists any remaining leading prompt system/developer messages and relocates directive-only `output_config` envelopes away from `messages[0]`, preventing the `messages.0: use the top-level 'system' parameter` HTTP 400 on model switches ([#12668](https://github.com/diegosouzapw/OmniRoute/pull/12668)). — thanks @insoln
- Honor a model's declared `reasoning_efforts` vocabulary in the reasoning-routing rule gate: a model-scoped or connection-scoped rule forcing `max`/`ultra` is now treated as supported when the model's resolved capabilities list that tier (operator overrides apply to models without a static registry declaration), instead of being rejected by the hardcoded `gpt-5.6-*` regex. Custom OpenAI-compatible providers whose models accept `max` natively (for example Merge Gateway `zai/glm-5.3-flash`, which accepts `low|high|max`) can now use forced-max rules without the request failing with `Reasoning effort 'max' is not supported by the configured target`. ([#12686](https://github.com/diegosouzapw/OmniRoute/pull/12686)) — thanks @woodsonl
- **fix(open-sse):**`reasoning_details[].text` is now promoted to `reasoning_content` even when `reasoning` is also present, so OpenRouter thinking models (GLM-5.3-Flash, DeepSeek-V4-Flash, Kimi K3) no longer lose their thinking traces in clients that only read `reasoning_content` ([#12688](https://github.com/diegosouzapw/OmniRoute/pull/12688) — thanks @thomasmaerz)
- **fix(providers):** xAI requests no longer silently drop an assistant tool call sent in the legacy OpenAI `function_call` shape (instead of `tool_calls[]`) — the call is now translated into the xAI request the same way modern tool calls are (#12692) ([#13753](https://github.com/diegosouzapw/OmniRoute/pull/13753)) — thanks @soroush5
- **fix(providers):** xAI responses no longer report `total_tokens`/`totalTokenCount` as `0` when upstream usage uses the legacy `prompt_tokens`/`completion_tokens` names instead of `input_tokens`/`output_tokens` (#12700) ([#13753](https://github.com/diegosouzapw/OmniRoute/pull/13753)) — thanks @soroush5
- **fix(dashboard):** the Modal provider connection form now shows a Base URL field (placeholder `https://<workspace>--<app>.modal.run/v1`), so bring-your-own-deploy Modal connections can be validated and saved instead of failing outright — the server-side validator already required `providerSpecificData.baseUrl` ([#12704](https://github.com/diegosouzapw/OmniRoute/issues/12704)) ([#12736](https://github.com/diegosouzapw/OmniRoute/pull/12736)) — thanks @gonisulaimann
- **fix(cursor):** Kimi-k3 / kimi-k3-high on the Cursor provider sometimes emit tool calls by imitating the executor's own history narration ("Assistant called tool … with arguments: …") instead of using structured tool calls, so clients received raw narration text plus native generation delimiters with `finish_reason: "stop"` — and the leaked turn compounded on every subsequent request via history re-send; the cursor executor now detects this shape and reassembles it into a structured `tool_calls` entry in both streaming and non-streaming finalization paths, gated on "no structured tool calls yet" so healthy turns are untouched ([#12723](https://github.com/diegosouzapw/OmniRoute/pull/12723)) — thanks @patrykkopycinski
- **fix(sse):** route the `dario` and `9router` request bodies through the internal-marker strip before they are serialized upstream — both executors override `transformRequest()` without calling the base implementation, so the internal context-relay / universal-handoff markers (`_omnirouteSkipContextRelay`, `_omnirouteInternalRequest`, `_omnirouteSkipUniversalHandoff`) reached strict OpenAI-compatible gateways and got the call rejected with HTTP 400 "Unsupported parameter(s)" ([#12729](https://github.com/diegosouzapw/OmniRoute/issues/12729)) ([#12735](https://github.com/diegosouzapw/OmniRoute/pull/12735)) — thanks @gonisulaimann
- **fix(providers):** OpenAI-compatible model discovery now parses per-vendor-route `effort_values` (nested under `vendors.<vendor>.capabilities.reasoning` in `/v1/models`), intersected across vendor routes so a synced level is always honored on every route the model can land on; re-syncing a connection whose catalog declares this shape no longer silently resets the synced `supportedThinkingEfforts`/`defaultThinkingEffort` data ([#12730](https://github.com/diegosouzapw/OmniRoute/pull/12730)) — thanks @woodsonl
- **fix(models):** a cold `GET /v1/models` on a large deployment no longer blocks the event loop for about a second at a time or overruns the 8s cold-build bound: since #12046 the built-in `auto/*` combos resolved catalog metadata for every target of every combo without memoizing or yielding, and they all draw on the same candidate pool, so 720 synced models took the build from ~4s to ~18s. Each distinct target is now resolved once per build, with a yield between misses ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13678](https://github.com/diegosouzapw/OmniRoute/pull/13678))
- **fix(combo):** a `quota-share` combo whose steps carry no weight now rotates across its targets again instead of sending every request to the first one — the resolver turns an unset weight into 0 and #10881 made 0 mean "disabled", so an all-unweighted combo had no quanta and fell back to definition order; an explicit 0 still disables a target next to weighted siblings ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13678](https://github.com/diegosouzapw/OmniRoute/pull/13678))
- **fix(codex):** the Codex WebSocket transport now emits a terminal `response.failed` (code `upstream_websocket_closed`) when the upstream socket closes before a terminal response event, instead of ending the client stream as if it had completed normally — preventing silent output truncation and allowing fallback/retry to trigger ([#12737](https://github.com/diegosouzapw/OmniRoute/pull/12737)). — thanks @insoln
- **fix(models):** preserve free-model metadata (`isFree`) discovered live from a provider through synced-model normalization, so free models no longer lose that flag before reaching the UI/consumers ([#12763](https://github.com/diegosouzapw/OmniRoute/pull/12763)) — thanks @keeltrace
- `/v1/models` combos whose merged `capabilities.vision` is `true` now also advertise `input_modalities: ["text","image"]` / `output_modalities: ["text"]` (synced modality intersections keep precedence), so models.dev-shaped clients no longer see a vision combo as text-only. (#12799 — thanks @aref-alapour)
- **fix(db):** the background cleanup scheduler no longer runs a blocking full `VACUUM` after pruning rows (it froze every route, `/healthz` included, for minutes on large databases — 30 s after every start and every 6 h); freed pages are now reclaimed with paced `PRAGMA incremental_vacuum` batches plus a WAL checkpoint, and on `auto_vacuum=NONE` a full VACUUM is deferred to the Storage page's scheduled window via `vacuumScheduler.requestFullVacuum()` ([#12821](https://github.com/diegosouzapw/OmniRoute/issues/12821)) ([#12830](https://github.com/diegosouzapw/OmniRoute/pull/12830)) — thanks @insoln
- **fix(sse):** stop over-escaped tabs from `gpt-5.6-luna-xhigh` corrupting Codex tool-call arguments — `\\t` is now collapsed back to a real tab instead of a literal `\t` text ([#12841](https://github.com/diegosouzapw/OmniRoute/pull/12841)) — thanks @rafacpti23
- **fix(resilience):** a recoverable direct-fetch response-start timeout (`DIRECT_RESPONSE_START_TIMEOUT`) could, in a narrow timer/promise-settlement race, escape as an `unhandledRejection` → `uncaughtException` and kill the server process — even though `proxyFetch` already retries this exact condition on a fresh socket. Guarded the timer callback so it can no longer fire against an already-settled attempt, and extended the process-level crash guard (already used by the WS/API-bridge servers) to recognize and swallow this code if it ever escapes anyway. Also installs that same guard in the production server entrypoint (`dist/server-ws.mjs`), which never had it even though the dev server already did ([#12861](https://github.com/diegosouzapw/OmniRoute/issues/12861)) ([#13636](https://github.com/diegosouzapw/OmniRoute/pull/13636)) — thanks @insoln / @HouMinXi
- **fix(translator):** Gemini to Claude usage no longer double-counts the cached prompt prefix — `input_tokens` now excludes `cache_read_input_tokens`, matching the Anthropic Messages semantics ([#12863](https://github.com/diegosouzapw/OmniRoute/pull/12863)) — thanks @ThiagoMafra-Integrare
- **fix(sse):** An Anthropic OAuth `403 "Request not allowed"` no longer bans the Claude connection on the first response — it is a per-request refusal on an otherwise healthy token, so it is now classified as the non-terminal `request_rejected` type, the connection is excluded for a growing cooldown (5 min, then 15 min) and only three consecutive refusals with no success in between escalate to `banned`; previously a single such response flipped the only Claude connection to `banned` and every later request was short-circuited with "All 1 connection(s) banned by upstream" until an operator reconnected ([#12859](https://github.com/diegosouzapw/OmniRoute/issues/12859), [#12864](https://github.com/diegosouzapw/OmniRoute/pull/12864) — thanks @insoln)
- fix(cache): never write a truncated completion (`finish_reason: "length"`/`max_tokens`) into the semantic cache — a partial answer cached under a temperature:0 signature was served to every later identical request, permanently returning a mid-sentence reply that no retry cleared (#12885) ([#12885](https://github.com/diegosouzapw/OmniRoute/pull/12885)) — thanks @patrykkopycinski
- **fix(providers):** vLLM connections now advertise the real context window: model discovery reads `max_model_len` instead of falling back to the 128K default ([#12897](https://github.com/diegosouzapw/OmniRoute/pull/12897), closes [#12858](https://github.com/diegosouzapw/OmniRoute/issues/12858)) — thanks @ntdat812
- **fix(combos):** A combo's visibility can be changed through the API again: `updateComboSchema` accepts `isHidden`, so a visibility-only update is no longer rejected as empty and a mixed update no longer drops it ([#12898](https://github.com/diegosouzapw/OmniRoute/pull/12898), closes [#12836](https://github.com/diegosouzapw/OmniRoute/issues/12836)) — thanks @ntdat812
- fix(guardrails): Vision Bridge now extracts and replaces base64 images nested inside a `tool_result.content` array (the shape Claude Code uses), not just top-level content parts — previously these requests silently reached a vision-incapable provider and returned a 400. Also resolves a provider prefix that has no known alias (e.g. a custom OpenAI-compatible connection's model prefix) to the node id its credentials are actually stored under, instead of discarding the operator's fixed model and falling back to a no-auth candidate that fails (#12903) — thanks @initguru
- fix(sse): inject the operator's global system prompt once, after request translation, for every target shape (Claude, Gemini, OpenAI Responses, OpenAI/Codex messages) instead of before translation — the pre-translation injection could be lost, repositioned, or duplicated 2-3× depending on the target format, and never reached the Responses API path at all. The new `injectSystemPromptPostTranslation()` is idempotent per request via a non-enumerable marker (#12904) — thanks @initguru
- **fix(sse):** strip echoed system/directive preamble on /v1/messages responses and preserve large analysis/summary blocks in systemPreambleStripper to stop autocompact empty-response ([#12905](https://github.com/diegosouzapw/OmniRoute/pull/12905)) — thanks @initguru
- **fix(sse):** thread the client's thinking intent into the non-streaming translation path so the same request answered with `stream:false` no longer leaks a thinking block that `stream:true` withholds ([#12905](https://github.com/diegosouzapw/OmniRoute/pull/12905)) — thanks @initguru
- **fix(sse):** gate thinking block emission on requestedThinking (streaming + non-stream) and flush reasoning-only responses as text to stop reasoning leak, autocompact loops, and 502 ([#12905](https://github.com/diegosouzapw/OmniRoute/pull/12905)) — thanks @initguru
- **fix(sse):** make the system-preamble stripper opt-in (`OMNIROUTE_STRIP_SYSTEM_PREAMBLE=1`) and flush both preamble strippers at stream end, so English-prose heuristics no longer delete a legitimate section of every openai→claude reply and an unterminated echo block no longer reaches the client as an empty message ([#12905](https://github.com/diegosouzapw/OmniRoute/pull/12905)) — thanks @initguru
- **fix(sse):** make the direct response-start timeout reasoning-aware — detect reasoning_effort high/max in the body and raise the ceiling to 180s to stop 504 on high-effort TTFB ([#12906](https://github.com/diegosouzapw/OmniRoute/pull/12906)) — thanks @initguru
- **fix(compat):** preserve GPT and Claude Code tool-call history when translating OpenAI Responses API requests to Chat Completions — `role:"tool"` items and role-based assistant `tool_calls` are no longer dropped, fixing incomplete multi-turn history ([#12909](https://github.com/diegosouzapw/OmniRoute/pull/12909)) — thanks @initguru
- **fix(usage):** finalize semantic cache hits by exact request id — `checkSemanticCache` now uses `finalizePendingScope(pendingScope, ...)` instead of an ambiguous (model, provider, connectionId) tuple, fixing wrong-request finalization when connectionId is null or multiple requests are in flight on the same connection ([#12910](https://github.com/diegosouzapw/OmniRoute/pull/12910)) — thanks @initguru
- **fix(sse):** the Codex Responses WebSocket bridge now fails fast to another eligible account instead of queueing behind a saturated one, releasing its per-account lease exactly once when the session ends ([#12911](https://github.com/diegosouzapw/OmniRoute/pull/12911) — thanks @initguru). This also fixes `accountSemaphore`'s `maxQueueSize: 0` handling, which previously behaved as an unbounded queue instead of failing over immediately — benefiting every caller that configures `queueDepth: 0` (for example combo routing), not just the Codex WS bridge.
- **fix(chatcore):** block a client's own duplicate retry (same idempotency key) from opening a second upstream turn while the first is still in flight, returning `409 turn_in_progress` instead of wasting quota on a redundant execution ([#12912](https://github.com/diegosouzapw/OmniRoute/pull/12912)) — thanks @initguru
- fix(sse): bound streams that keep sending raw upstream bytes forever without ever emitting a terminal event — a new `STREAM_ACTIVE_TIMEOUT_MS` watchdog (default 1260000ms/21min — the largest per-model `timeoutMs` in the registry plus a one-minute margin, `0` disables) tracks the stream's total lifetime independently of the existing byte-stall watchdog, so a continuously-active non-terminal stream can no longer occupy a connection indefinitely (#12913) — thanks @initguru
- **fix(providers):** correct Magnific API key validation, which reported every valid key as invalid due to a GET probe against a POST-only endpoint (#12927) ([#13754](https://github.com/diegosouzapw/OmniRoute/pull/13754)) — thanks @hubo1989
- **fix(routing):** Auggie now fails over to the next combo model instead of returning the quota-exhausted CLI warning as a successful reply (#12949) ([#13751](https://github.com/diegosouzapw/OmniRoute/pull/13751)) — thanks @honeypot55
- **fix(combo):** A weighted combo whose every target was excluded before dispatch by a resilience timer (model lockout, open circuit breaker, provider cooldown) now answers `503``all_targets_cooling_down` with `Retry-After` set to the earliest exclusion to lapse, the excluded targets and reasons in `diagnostics.excluded`, a `wait` recovery hint, and a `[COMBO]` warning naming the reasons; previously the pool was dropped silently and the host answered `404 "Combo has no executable targets"` (recovery hint "switch combo / reconnect the missing providers") for a pool that was configured, connected and merely cooling down — which clients such as Claude Code render as "this model may not exist". A pool with nothing to run keeps its `404` ([#12954](https://github.com/diegosouzapw/OmniRoute/issues/12954), [#12956](https://github.com/diegosouzapw/OmniRoute/pull/12956) — thanks @insoln)
- **fix(resilience):** A `5xx` model-lockout failure — a transport error (`terminated`, `EHOSTUNREACH`, connect timeout), an upstream server error, or OmniRoute's own synthesized `502` from quality validation — now locks only the exact provider/connection/model tuple instead of the quota family; previously one empty response on a single `gpt-5.6-*` model removed every `gpt-5*` model of the codex connection from routing for 2–30 min (escalating) while its quota was untouched. Quota statuses (`429`/`403`/`402`) keep the family scope; success-decay and the Model Cooldowns card now handle exact-scope locks too ([#12955](https://github.com/diegosouzapw/OmniRoute/issues/12955), [#12957](https://github.com/diegosouzapw/OmniRoute/pull/12957) — thanks @insoln)
- **fix(providers):** GitLab Duo Retest and chat requests now fall back to the public Code Suggestions endpoint for ANY `direct_access` 403 (not only the "direct connections are disabled" tenant-config message), and surface the real upstream error body instead of a generic "Access denied" when both endpoints reject the token (#12958) ([#13758](https://github.com/diegosouzapw/OmniRoute/pull/13758)) — thanks @Rahulsharma0810
- **fix(sse):** stop misclassifying a truncated Anthropic-compatible `max_tokens` probe response (`content:[{type:"text",text:""}]`) as an empty upstream response (#12968) ([#13771](https://github.com/diegosouzapw/OmniRoute/pull/13771)) — thanks @pranay-gpt
- **fix(images):** image-combo legs now fall back when an upstream provider returns HTTP 2xx with an empty or malformed image payload. `fetchImageEndpoint` previously normalized any successful HTTP response to `success: true` (`data.data || []`), so `executeImageCombo` stopped on the first leg and handed the client an image-less 200. The OpenAI-compatible normalization now requires at least one usable item (non-empty `b64_json` or `url`) in `data[]`; an empty/malformed 2xx becomes a retryable 502 with a sanitized error, so priority image combos advance to the next leg. Valid payloads and direct image-model requests are unchanged. [#12982](https://github.com/diegosouzapw/OmniRoute/pull/12982) — thanks @tiangao88
- **fix(claude):** forward client-negotiated `thinking-binding-controls-2026-08-01` and `thinking-display-updates-2026-08-18` betas so Fable 5.1 `thinking.block_binding` / `thinking.display` requests are no longer rejected upstream with `Extra inputs are not permitted` ([#12989](https://github.com/diegosouzapw/OmniRoute/pull/12989)) — thanks @fidelix
- **fix(cli):** skip the POSIX CLI path lookup during Windows autostart setup, preventing a bogus path error before successful enablement ([#12993](https://github.com/diegosouzapw/OmniRoute/pull/12993)) — thanks @zachary-frederich
- **fix(i18n):** stop the "Saving..." hang on `/dashboard/combos` — `BuilderIntelligentStep.tsx`'s exploration-rate hint called `t("explorationRateHint")` with no ICU values even though the key requires `{percent}`, and next-intl's default error handling throws `FORMATTING_ERROR` for that call, unmounting the whole builder step and looking like a silent save hang. Fixed across all three affected call sites (`BuilderIntelligentStep.tsx`, `AgentBridgeMaintenanceCard.tsx`, `RawJsonPanel.tsx`), not just the one that was reported ([#12995](https://github.com/diegosouzapw/OmniRoute/pull/12995)). — thanks @hartmark
- **fix(api):** restore the MCP `namespace` field on streamed and non-streamed Responses tool calls in follow-up turns of a session that don't re-declare their `type:"namespace"` tools (#12996) ([#13769](https://github.com/diegosouzapw/OmniRoute/pull/13769)) — thanks @rolemiaster
- **fix(db):** add an opt-in automatic sweep for terminal batch checkpoints and expired file content, behind the new `BATCH_AND_FILE_AUTO_CLEANUP_ENABLED` feature flag (default off) — `batch_item_checkpoints` had grown to 182K rows / 5.25 GB with no batch ever explicitly deleted by an operator: the manual `delete-completed` route existed but nothing called it automatically, and it never covered failed/cancelled/expired batches either. Extracts a shared `deleteBatchesMatching()` (age-gated, every terminal status, keeping `deleteCompletedBatches()`'s exact existing contract) and adds `pruneExpiredFiles()` for uploaded file content past its own `expires_at` (1,874 rows / 5.19 GB observed live, most long past expiry). With the flag off (the default), every existing install keeps this data exactly as before; an operator must opt in via the dashboard or `BATCH_AND_FILE_AUTO_CLEANUP_ENABLED=true` before the sweep deletes anything ([#12999](https://github.com/diegosouzapw/OmniRoute/pull/12999)). — thanks @hartmark
- fix(providers): stop `@omniroute/opencode-plugin` combo context limits from downgrading to the raw `Math.min(member)` lower bound after a restart — the static catalog now honors the server-computed `computed_context_length` (mirroring the dynamic hook), and a background refresh with a degraded `/api/combos` response backfills the field from the last-known-good disk snapshot instead of overwriting it (#13000) ([#13759](https://github.com/diegosouzapw/OmniRoute/pull/13759)) — thanks @morpheus9393
- **fix(streaming):** allow a per-provider override of the fetch-start (headers-wait) timeout cap so providers that buffer the full generation before the first byte (e.g. `command-code`, `opencode-go`) are not cut off at the global 110s cap; the same two entries also gain a reasoning-safe `requestDefaults.maxTokens` of 16384 so thinking models such as `z-ai/glm-5.3-flash` are not cut off mid-reasoning ([#13002](https://github.com/diegosouzapw/OmniRoute/pull/13002)) — thanks @alvinveroy
- **fix(resilience):** An apikey-category 429 whose body explicitly says a long-window quota was exhausted no longer skips the quota cache — `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`) gained an `errorText` parameter in the #6638 fix, but only one of its two call sites was updated: `checkFallbackError()` passes the upstream body while `shouldMarkAccountExhaustedFrom429()` (`open-sse/services/accountFallback.ts`) still called it with the provider alone. With `errorText` undefined the helper's `Boolean(errorText) && looksLikeQuotaExhausted(errorText)` branch can never be true, so for every apikey-category provider without per-model quotas the connection was never marked quota-exhausted. `errorText` is now threaded through the helper and passed at the `src/sse/handlers/chat.ts` call site. Plain rate limits (`Rate limit exceeded, retry in 20s`, `Too Many Requests`) still fall through to the short generic cooldown. Regression guard: `tests/unit/quota-signal-errortext-threading.test.ts`. ([#13008](https://github.com/diegosouzapw/OmniRoute/pull/13008)) — thanks @Rick7C2
- **fix(cli):**`omniroute mcp restart` no longer 404s — the missing `POST /api/mcp/restart` route now exists — and new `omniroute mcp enable`/`mcp disable [--transport]` subcommands give the CLI a way to turn the MCP server on without the dashboard ([#13012](https://github.com/diegosouzapw/OmniRoute/issues/13012)) ([#13770](https://github.com/diegosouzapw/OmniRoute/pull/13770)) — thanks @ricardusx
- **fix(mitm):** add catch-all (*) model mapping fallback for Agent Bridge ([#13013](https://github.com/diegosouzapw/OmniRoute/pull/13013)) — thanks @tuandinh0801
- **fix(dev):** allow Ctrl+C to promptly kill dev server by closing active connections ([#13020](https://github.com/diegosouzapw/OmniRoute/pull/13020)) — thanks @tuandinh0801
- **fix(skills):** repair nested malformed skill-tool schemas (bare property maps, boolean `required: true`) for OpenAI-compatible providers, not just the schema root (#13022) ([#13772](https://github.com/diegosouzapw/OmniRoute/pull/13772)) — thanks @ftevxk
- **fix(sse):** reasoning replay now works for Chat Completions and Anthropic Messages clients on Responses-API reasoning targets such as `opencode-go/deepseek-v4-flash`: plain (non-tool-call) assistant turns are captured against the same normalized transcript the read side digests (the Responses body carries `input`, not `messages`, so the write side digested only the assistant message instead of the full transcript and every replay missed), and the replay pass runs on the OpenAI pivot for every source format, so Anthropic Messages clients are replayed too. Fixes the intermittent `400 The reasoning_text in the thinking mode must be passed back to the API` from Console Go for clients that drop `reasoning_content` ([#13031](https://github.com/diegosouzapw/OmniRoute/pull/13031)) — thanks @jmche
- **fix(friendliai):** FriendliAI's free-tier credit-exhaustion 403 (`{"detail":"You've exhausted all your credits..."}`) is now classified as `QUOTA_EXHAUSTED` instead of `AUTH_ERROR`, so omniroute treats it as depleted credits rather than a credential problem ([#13040](https://github.com/diegosouzapw/OmniRoute/pull/13040)) — thanks @turbolego
- fix(oauth): kimi-coding/github device-flow `pollToken` no longer rejects with `TypeError: Body is unusable` when the token endpoint returns a non-JSON error page (CDN/anti-bot/proxy interstitial) — the body is now read once and parsed, preserving the graceful `invalid_response` fallback instead of a generic 500 (#13046 — thanks @ysntony)
- **fix(providers):** lazily load `chatgpt-web-codex` admin helpers in `PUT /api/providers/[id]`, mirroring #12355's exact pattern for the sibling `POST` route — a source-inspection test pins the import-graph invariant, since Turbopack's client-bundle boundary can't be exercised from the test harness directly ([#13071](https://github.com/diegosouzapw/OmniRoute/pull/13071)). — thanks @hartmark
- **fix(providers):** honor an operator-set endpoint override (`PUT /api/provider-models`) for a local model whose own `/v1/models` response carries no capability data of its own (llama.cpp included) — the override previously only took effect when a matching `customModels` entry already existed, so declaring a brand-new local model as embeddings-capable silently did nothing. `updateCustomModel()` gains an opt-in `createIfMissing` mode; every other caller's existing contract is unchanged. Also accepts a collapsed single-slash id for path-based local models ([#13078](https://github.com/diegosouzapw/OmniRoute/pull/13078)). — thanks @hartmark
- fix(quota): keep Kiro active while any _freetrial pool has quota (#13088) ([#13324](https://github.com/diegosouzapw/OmniRoute/pull/13324)) — thanks @giauphan
- **fix(routing):** round-robin combos now show up in Combo Studio's Live dashboard — they were completing successfully but never publishing the attempt/success/failure events the dashboard listens for (#13089) ([#13776](https://github.com/diegosouzapw/OmniRoute/pull/13776)) — thanks @adityadwi21
- **fix(sse):** stop rejecting a Responses API `tool_choice.type: "custom"` (e.g. Codex CLI forcing `functions__exec`) with a 400 `unsupported_feature` error (#13122) ([#13775](https://github.com/diegosouzapw/OmniRoute/pull/13775)) — thanks @phamtienduceng-eng
- **fix(translator):** recognize `tool_choice.type: "custom"` in Responses→Chat translation and propagate custom tool names (including namespace-flattened ones) across both the streaming and non-streaming provider legs, so non-streaming Responses clients get `custom_tool_call`/raw `input` instead of `function_call`/JSON arguments ([#13128](https://github.com/diegosouzapw/OmniRoute/pull/13128)) — thanks @ducphamtien-fonos
- **fix(providers):** remove the `chipotle`/`pepper` provider — its upstream (`amelia.chipotle.com`) now 404s on every route and is fully decommissioned (#13131, #4037) ([#13913](https://github.com/diegosouzapw/OmniRoute/pull/13913)) — thanks @Falco20100
- **fix(db):** authenticated `GET /api/db/health` polls no longer run a SQLite `quick_check`. The health dashboard polls every 15 seconds, and that scan ran synchronously on the request-serving event loop, blocking it for the length of the scan. Reference and state checks still run, and explicit repair requests keep integrity checks unless `OMNIROUTE_SKIP_DB_HEALTHCHECK=1` is set ([#13149](https://github.com/diegosouzapw/OmniRoute/pull/13149)) — thanks @cryptiklemur
- **fix(sse):** fail over once to a sibling connection on stream early EOF (the original `STREAM_EARLY_EOF` 502 is kept when no sibling can serve the request), gated behind `STREAM_EARLY_EOF_SIBLING_FAILOVER_ENABLED` (default off) ([#13153](https://github.com/diegosouzapw/OmniRoute/pull/13153)) — thanks @maxmad64bis
- **resilience:** a Cloudflare managed challenge (`cf-mitigated: challenge` / challenge HTML on 403) is classified as a fingerprint rejection and retried on another account/transport instead of banning the connection (#13161 ([#14090](https://github.com/diegosouzapw/OmniRoute/pull/14090)) — thanks @anhtran-ai)
- **fix(cli):** OpenCode config generator preserves catalog display names — custom names win, then `display_name`/native `name` (with the `owned_by/` prefix stripped once), then a readable label for `auto/*` ids, instead of always showing the raw model id ([#13168](https://github.com/diegosouzapw/OmniRoute/pull/13168)) — thanks @domenicomassafra
- **fix(sse):** Fable 5 and 5.1 keep their prompt-cache prefix across mid-conversation system messages. OmniRoute treated only Opus as capable, so every Fable system turn was hoisted into the top-level system prompt and moved the cached prefix, which reported `system_changed` from turn 2 on. Fable now has its own mid-conversation-system capability path, and `context-1m` stays limited to Opus so Fable is never sent an unrelated beta header ([#13173](https://github.com/diegosouzapw/OmniRoute/pull/13173)) — thanks @cryptiklemur
- **fix(combo):** auto-resume a pinned native Codex turn on a healthy sibling connection or model when the pinned provider becomes unavailable for a model-scoped reason (quota, model lockout) instead of failing the turn outright — provider-wide circuit-breaker/cooldown state, pending tool calls, opaque continuation state, and partial streams still block resume, and at most one auto-resume happens per logical turn ([#13180](https://github.com/diegosouzapw/OmniRoute/pull/13180)) ([#14162](https://github.com/diegosouzapw/OmniRoute/pull/14162)) — thanks @mdigitalbh81
- **fix(evals):** an eval case whose model call errored is no longer scored as passed — a case that never reached a model has no measured behaviour to grade ([#13201](https://github.com/diegosouzapw/OmniRoute/pull/13201)) — thanks @aaustinhuang / @dajiaohuang
- **fix(evals):** the eval runner now sends `x-omniroute-compression: off` and `x-omniroute-no-memory: true` on every case, so a graded case measures the model instead of the operator's injected output style, retrieved memory and `memory_*` tools ([#13139](https://github.com/diegosouzapw/OmniRoute/issues/13139), [#13206](https://github.com/diegosouzapw/OmniRoute/pull/13206)) — thanks @aaustinhuang / @dajiaohuang
- **fix(combos):** stop dropping live keys and persisting dead ones ([#13217](https://github.com/diegosouzapw/OmniRoute/pull/13217)) — thanks @maxmad64bis
- **fix(db):** the WAL checkpoint busy counter reported by `/api/monitoring/health` now survives restarts — busy checkpoints are counted in memory and persisted from the next clean maintenance tick or at shutdown, never with a write while the database is contended ([#13218](https://github.com/diegosouzapw/OmniRoute/pull/13218)) — thanks @maxmad64bis
- **fix(vertex):** preserve Claude prompt-cache breakpoints for Vertex and Vertex Partner, use the documented five-minute ephemeral TTL by default, and forward cache usage metadata through streaming responses ([#13220](https://github.com/diegosouzapw/OmniRoute/pull/13220)) — fixes #13219 — thanks @SIGTERM-015
- **fix(mcp):** load the audit `better-sqlite3` driver via the shared `runtimeRequire()` helper instead of `createRequire(import.meta.url)`, which broke when the Next.js standalone build emits the module as a CommonJS chunk ([#13223](https://github.com/diegosouzapw/OmniRoute/pull/13223)) — thanks @chatchawan-simplewish
- **fix(sse):** classify a missing Playwright Chromium install on the Z.ai web transport as an actionable 503 host/config cooldown instead of a generic 502 that trips the provider circuit breaker (#13232) ([#13777](https://github.com/diegosouzapw/OmniRoute/pull/13777)) — thanks @oleksandr1811
- **fix(combos):** testing a combo aborts in-flight probes when the client disconnects instead of probing on after the dashboard navigates away ([#13279](https://github.com/diegosouzapw/OmniRoute/pull/13279)) — thanks @maxmad64bis
- **fix(quota):** in-process routing and quota caches (quality tracker, saturation and rate-limit header caches, quota-fetcher cache, learned rate limits, account buckets) are now size-bounded through one shared `boundedMap` — caps sit far above normal deployments, evictions are logged once per minute per cache instead of per entry, and state whose loss would change routing (live saturated quota buckets, evaluator quality scores) is never evicted ([#13280](https://github.com/diegosouzapw/OmniRoute/pull/13280)) — thanks @maxmad64bis
- **fix(call-logs):** call-log error types are now a versioned vocabulary (`ERROR_TYPE_CONTRACT v1`) with explicit `unknown` instead of ambiguous `null`, and free-text history reads back as `unclassified` ([#13281](https://github.com/diegosouzapw/OmniRoute/pull/13281)) — thanks @maxmad64bis
- Fixed tests leaving temp `DATA_DIR` folders behind on Windows by closing the SQLite handle before removing the directory (#13290). ([#13292](https://github.com/diegosouzapw/OmniRoute/pull/13292)) — thanks @anhtahaylove
- **fix(providers):** honor the selected Alibaba workspace and region endpoints for custom embedding and `qwen3-rerank` requests ([#13293](https://github.com/diegosouzapw/OmniRoute/pull/13293)) — thanks @xiaoyaner0201
- **fix(backend):** error messages are no longer truncated after a path — `redactErrorPaths` treated any slash-bearing span as an unequivocal filesystem path and swallowed the rest of the line, so the image-model 400 lost the `Use POST /v1/images/generations instead.` hint it exists to give, and a redacted diagnostic lost its ` with api_key='[REDACTED]'` tail. Only a Windows path, file URI or known POSIX root with no determinable end swallows the line now ([#13144](https://github.com/diegosouzapw/OmniRoute/issues/13144)) ([#13295](https://github.com/diegosouzapw/OmniRoute/pull/13295)) — thanks @abhisheksharma2411 / @ggiak
- **fix(providers):** Fetch Qwen and Alibaba Token Plan model catalogs through their authenticated console gateways, with public-only URL validation and local-catalog fallback when discovery is unavailable. ([#13299](https://github.com/diegosouzapw/OmniRoute/pull/13299)) — thanks @JxnLexn
- **fix(db):** defer `process.exit(0)` on graceful shutdown by one macrotask, avoiding a Windows-only libuv abort when the sql.js fallback driver has a statement in flight (#13306) ([#13778](https://github.com/diegosouzapw/OmniRoute/pull/13778)) — thanks @anhtahaylove
- **fix(cli):**`omniroute serve` now surfaces a fatal `[STARTUP] Fatal: ...` boot diagnostic (e.g. a DB driver init failure) to the console immediately, even without `--log`, instead of only when the process later crashes or restarts (#13314) ([#13779](https://github.com/diegosouzapw/OmniRoute/pull/13779)) — thanks @Orion1943
- **fix(sse):** A transient error on a round-robin combo target no longer resets its concurrency limit to 3, so a target capped at 1 is not sent three queued requests at once when its cooldown ends ([#13320](https://github.com/diegosouzapw/OmniRoute/pull/13320)) — thanks @datrixlab
- **fix(resilience):** Rate-limit reset headers in RFC 3339 form (Anthropic) and with fractional seconds (`2m59.56s`) are parsed correctly, instead of an Anthropic reset seconds away throttling the connection for about 34 minutes ([#13321](https://github.com/diegosouzapw/OmniRoute/pull/13321)) — thanks @datrixlab
- **fix(cli):**`contexts export --no-secrets` now leaves the access tokens and API keys out, and `chat --no-history` and `serve --no-recovery` take effect; all three flags were accepted and ignored ([#13322](https://github.com/diegosouzapw/OmniRoute/pull/13322)) — thanks @datrixlab
- **fix(providers):** Switching "Allow Private Provider URLs" off in the dashboard now takes effect when `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS=true` is set in the environment ([#13323](https://github.com/diegosouzapw/OmniRoute/pull/13323)) — thanks @datrixlab
- **fix(cli):**`omniroute restart` now comes back on the port set by `PORT` (shell or `<DATA_DIR>/.env`) instead of always 20128, like `serve` and `dashboard` ([#13327](https://github.com/diegosouzapw/OmniRoute/pull/13327)) — thanks @datrixlab
- **fix(redis):** Warmup circuit-breaker keys now honor `REDIS_KEY_PREFIX` like every other OmniRoute Redis key, instead of always using `omniroute:warmup:cb:` ([#13328](https://github.com/diegosouzapw/OmniRoute/pull/13328)) — thanks @datrixlab
- **fix(api):** Setting `DISABLE_SQLITE_AUTO_BACKUP=true` no longer makes the API-key rate limiter and auth cache skip Redis, which let every replica enforce the full per-key limit on its own ([#13329](https://github.com/diegosouzapw/OmniRoute/pull/13329)) — thanks @datrixlab
- **fix(translator):**`tool_choice: "none"` is now sent to Claude-format providers as `{ type: "none" }` (and back to OpenAI as `"none"`) instead of `auto`, so the model can no longer call tools the client switched off ([#13333](https://github.com/diegosouzapw/OmniRoute/pull/13333)) — thanks @datrixlab
- **fix(translator):** Gemini tool results sent without an `id` (the usual case, since OmniRoute's own Gemini responses never emit one) now reach the model instead of being replaced by an empty result, on the Gemini, Antigravity and `/v1beta` request paths ([#13334](https://github.com/diegosouzapw/OmniRoute/pull/13334)) — thanks @datrixlab
- **fix(translator):** An image returned inside a Claude `tool_result` (Read on a PNG, an MCP screenshot) is now sent to Gemini as an image part instead of base64 text ([#13335](https://github.com/diegosouzapw/OmniRoute/pull/13335)) — thanks @datrixlab
- **fix(combo):** stop retrying a malformed-request-shape error across the entire fallback chain — a 400/422 that fails one target for a `kind: "model"` reason (exact status + error message match) will fail identically on every other target too, so a bad payload previously burned the full `MAX_GLOBAL_ATTEMPTS` budget instead of failing fast. Observed live: 41-44 identical combo decisions over 13+ minutes for a single request. Trips only on 3 consecutive identical `kind: "model"` failures, leaving transient and provider-side errors untouched ([#13338](https://github.com/diegosouzapw/OmniRoute/pull/13338)). — thanks @hartmark
- **fix(db):**`getDbInstance()` now closes the probe and primary SQLite connections on every failed initialization path, not just the happy path, fixing a handle leak that caused `EPERM` on Windows teardown. ([#13303](https://github.com/diegosouzapw/OmniRoute/issues/13303)) ([#13342](https://github.com/diegosouzapw/OmniRoute/pull/13342)) — thanks @voidstackloop
- **fix(dispatch):** strip every `_omniroute*` internal marker at the shared pre-serialization chokepoint (`cliFingerprints.ts`) instead of a hand-maintained per-key allowlist, so internal routing/handoff markers (e.g. `_omnirouteSkipContextRelay`, `_omnirouteResponsesStore`) can no longer leak into serialized upstream request bodies and draw `400 Extra inputs are not permitted` from strict Anthropic-compatible gateways ([#12729](https://github.com/diegosouzapw/OmniRoute/issues/12729), fixed in [#13355](https://github.com/diegosouzapw/OmniRoute/pull/13355)) — thanks @patrykkopycinski
- **fix(providers):** stop zed-hosted `claude-haiku-4-5` extended-thinking requests from inflating `max_tokens` past the model's real 64000 output cap (#13364) ([#13780](https://github.com/diegosouzapw/OmniRoute/pull/13780)) — thanks @ThiagoMafra-Integrare
- **fix(providers):** gemini-web no longer drops the system instruction on single-turn requests or the tool contract when a client system message is present, and switches to an atomic composer insert so embedded newlines can't submit the message early (#13380) ([#13784](https://github.com/diegosouzapw/OmniRoute/pull/13784)) — thanks @formilw
- **fix(providers):**`gemini-web` now attempts to select and verify the requested Gemini UI mode (and Extended Thinking) before answering, and fails closed with a clear 400 instead of silently running the account default under a mismatched model label (#13381) ([#13919](https://github.com/diegosouzapw/OmniRoute/pull/13919)) — thanks @formilw
- **fix(db):** stop routine connection-backoff auto-recovery from busting the entire `/v1/models` response cache, which was causing intermittent 75-120s/502 responses on deployments routing many providers (#13389) ([#13783](https://github.com/diegosouzapw/OmniRoute/pull/13783)) — thanks @RaviTharuma
- **fix(mitm):** bound per-request SSE transcript retention to 1 MiB and stop the upstream read when the downstream disconnects — handler-side `collected` strings grew without bound before the inspector clamp, and abandoned streams kept the reader alive for the full upstream lifetime ([#13395](https://github.com/diegosouzapw/OmniRoute/issues/13395)) ([#13702](https://github.com/diegosouzapw/OmniRoute/pull/13702)) — thanks @oyi77
- **fix(opencode-plugin):** The stale disk-cache fallback warning now reports the snapshot's age (`using stale disk cache (N models, age 168h)`), matching the existing warm-startup log. Previously a week-old catalog was indistinguishable from a five-minute-old one, so silent model drift went unnoticed. (#13426) — thanks @RaviTharuma
- **fix(api):**`GET /api/logs/export` now streams rows from the database itself (a cursor for `proxy-logs`, a LIMIT-bounded generator for `call-logs`/`request-logs`) instead of buffering every matching row in memory before serializing, fixing a V8 heap OOM on large tables ([#13428](https://github.com/diegosouzapw/OmniRoute/pull/13428), [#13123](https://github.com/diegosouzapw/OmniRoute/issues/13123)) — thanks @KooshaPari. **Breaking:** the `limit` query param now defaults to 10,000 rows (max 50,000) — exports that previously returned every matching row are silently truncated (with `"capped":true,"totalAvailable":<n>` in the response) unless the caller passes a larger explicit `limit`.
- **fix(compression):** stop lite compression from dropping a `role:"tool"` message when it is byte-identical to the previous message, which orphaned a `tool_call_id` and triggered upstream 400 errors on parallel tool calls (#13429) ([#13787](https://github.com/diegosouzapw/OmniRoute/pull/13787)) — thanks @tolgaaksoy
- **fix(sse):** frame post-keepalive `/v1/responses` stream errors with a top-level `type` field so Responses clients (Codex) surface the real upstream error instead of reporting "stream disconnected before completion" (#13431) ([#13785](https://github.com/diegosouzapw/OmniRoute/pull/13785)) — thanks @andrea-kingautomation
- **fix(db):** reconcile `auto_vacuum` drift between the configured INCREMENTAL mode and the live SQLite pragma — detected at startup and reconciled out-of-request by the vacuum scheduler, which now also runs a bounded `PRAGMA incremental_vacuum` reclaim instead of an unconditional full `VACUUM` once INCREMENTAL is actually in effect (#13432) ([#13786](https://github.com/diegosouzapw/OmniRoute/pull/13786)) — thanks @tolgaaksoy
- **fix(models):** Reconcile provider dashboards with confirmed authoritative live catalogs, excluding retired built-in/imported rows while preserving manual custom models and partial-catalog fallbacks. ([#13434](https://github.com/diegosouzapw/OmniRoute/pull/13434)) — thanks @JxnLexn
- **fix(build):** the production build no longer breaks when a client component reaches a server-only module, and the client-bundle guard now discovers server-only modules instead of matching a fixed list ([#13436](https://github.com/diegosouzapw/OmniRoute/pull/13436)) — thanks @maxmad64bis
- **fix(models):** return a retryable 503 with Retry-After instead of a 500 when the first catalog build outlasts its time bound ([#13438](https://github.com/diegosouzapw/OmniRoute/pull/13438)) — thanks @maxmad64bis
- **fix(combo):** new opt-in flag `PROTECTED_PRIORITY_INFRA_502_ENABLED` (default off): when a priority target marked fallback-only-on-quota-exhaustion stops the combo because its provider circuit breaker is open or a predictive latency check rejected it — causes that are provably not quota — the response is 502 instead of a quota-looking 503; lockout, cooldown, unavailable, exhaustion, credential-gate and concurrency-cap stops keep 503 ([#13439](https://github.com/diegosouzapw/OmniRoute/pull/13439)) — thanks @maxmad64bis
- **fix(resilience):** non-TPD daily-quota cooldowns honor the provider node's configured daily-reset clock (timezone + hour) instead of server midnight, on single-model and combo (priority and round-robin) paths; timezone edits apply without a restart ([#13440](https://github.com/diegosouzapw/OmniRoute/pull/13440)) — thanks @maxmad64bis
- **fix(call-logs):** the call-log write point validates `error_type` against the versioned vocabulary with a Zod schema and stores `unknown` for any value outside it, so a classifier family that drifts from `ERROR_TYPE_CONTRACT` can never persist free text ([#13441](https://github.com/diegosouzapw/OmniRoute/pull/13441)) — thanks @maxmad64bis
- **fix(oauth):** token health check now parses a numeric epoch `expires_at` (number or string, seconds or milliseconds), so connections synced by external tools keep their expiry-driven refresh instead of being skipped forever — or refreshed on every sweep ([#13444](https://github.com/diegosouzapw/OmniRoute/pull/13444)) — thanks @elielsousa-pathbit
- **fix(db):** Arena ELO sync now fetches and validates the leaderboards before touching `model_intelligence`, and applies the upsert + prune of expired rows inside a single atomic transaction. Previously, an unavailable/rate-limited Arena API left the table pruned with nothing written back, and since the sync runs on every boot, repeated restarts against a rate-limited upstream permanently drained the table to zero ([#13446](https://github.com/diegosouzapw/OmniRoute/pull/13446)) — thanks @CrashCartCapital
- **fix(analytics):** the compression analytics writer now passes `flatRateAsZero: true` to `calculateCost`, matching `/api/usage/analytics`. Flat-rate subscription lanes (minimax, glm, kimi, bailian, xiaomi, web-cookie) no longer report a dollar "savings" figure that was never actually payable ([#13446](https://github.com/diegosouzapw/OmniRoute/pull/13446)) — thanks @CrashCartCapital
- **fix(sse):** stop an unhydrated `openai-compatible-*`/`anthropic-compatible-*` connection from silently routing chat requests (and its stored credential) to the real OpenAI/Anthropic API instead of the operator's configured provider-node endpoint (#13452) ([#13798](https://github.com/diegosouzapw/OmniRoute/pull/13798)) — thanks @DenXio101
- **fix(sse):** Pollinations and Perplexity-web requests now fail over instead of returning the provider's own "out of credits"/"account suspended" text as if it were a real answer — an HTTP 200 body whose short assistant message is dominated by a known credits-exhausted or account-deactivated phrase is now classified as a malformed response and triggers the existing combo/auto-fallback path (#13461) ([#13910](https://github.com/diegosouzapw/OmniRoute/pull/13910)) — thanks @arjav1181
- **fix(resilience):** background OAuth token refresh (proactive health-check sweep and the shared refresh helper behind `refreshAccessToken`/`refreshClaudeOAuthToken`/etc.) now fails closed like the interactive chat path when a connection's assigned proxy pool is entirely dead, instead of silently sending the refresh-token exchange out direct or via a stray `HTTPS_PROXY` (#13470) ([#13793](https://github.com/diegosouzapw/OmniRoute/pull/13793)) — thanks @elielsousa-pathbit
- **fix(providers):** Muse Spark 1.3 works on OpenCode Zen, OpenCode and OpenCode Go instead of failing with a 500, and gets its real 1M context window ([#13471](https://github.com/diegosouzapw/OmniRoute/pull/13471)) — thanks @maxmad64bis (with thanks to @bacnh85, @shermzy and @atakhadiviom for #12675, #12973 and #13111)
- **fix(sse):** forward Anthropic prompt-cache-creation tokens through the `/v1/responses` usage hop so cache-write counts stop logging as zero (#13472) ([#13790](https://github.com/diegosouzapw/OmniRoute/pull/13790)) — thanks @fidelix
- **fix(opencode):** opt-in `OPENCODE_RESPONSES_STALL_ROTATION` flag (default off): a streamed Responses reply that sends headers and then nothing is cut after `RESPONSES_FIRST_BYTE_TIMEOUT_MS` (default 15 s) instead of waiting for the stream readiness timeout — the account is cooled down and the request rotates to the next account once (proxied or proxy-less), a second stall fails fast; with the flag off nothing changes ([#13484](https://github.com/diegosouzapw/OmniRoute/pull/13484)) — thanks @maxmad64bis
- **fix(sse):** stop the streaming PII sanitizer from splicing OpenRouter metadata (`provider`, `native_finish_reason`, `reasoning_details[].format`) into the answer text buffer (#13488) ([#13792](https://github.com/diegosouzapw/OmniRoute/pull/13792)) — thanks @Xore
- **fix(sse):** opt-in `OPENCODE_USER_BLOCKED_ROTATION` flag (default off): an opencode 403 or 451 carrying a `user_blocked` refusal cools the refused account down and fails over to the next account at most once per request, cancelling the abandoned response body; with the flag off the refusal is returned unchanged ([#13498](https://github.com/diegosouzapw/OmniRoute/pull/13498)) — thanks @maxmad64bis
- **fix(runtime):** eliminate hardcoded 20128 port remnants and make loopback URLs dynamic ([#13533](https://github.com/diegosouzapw/OmniRoute/pull/13533)) — thanks @ggdayup
- **fix(cli):** redraw the CLI/Electron system tray icon with a dark outline and ship a native multi-res `icon.ico` so it is no longer a pure-white, nearly invisible glyph on the Windows light-theme taskbar and hidden-icons flyout (#13535) ([#13797](https://github.com/diegosouzapw/OmniRoute/pull/13797)) — thanks @ProphetOfDoom-PoD
- **fix(cli):** persist the supervisor's give-up crash record to `<DATA_DIR>/server/crash.log` (surfaced by `omniroute doctor`) instead of only printing it — the console output was discarded when `--tray` mode's detached worker exited, leaving no trace of why the gateway/tray disappeared (#13538) ([#13908](https://github.com/diegosouzapw/OmniRoute/pull/13908)) — thanks @ProphetOfDoom-PoD
- **fix(api):**`/v1/audio/transcriptions`, `/v1/audio/translations` and `/v1/audio/speech` requests now show up in Dashboard → Request Logs — the three routes never called the shared call-log pipeline, so every successful (and failed) transcription/translation/speech request was silently dropped from `call_logs` ([#13544](https://github.com/diegosouzapw/OmniRoute/issues/13544)) ([#13803](https://github.com/diegosouzapw/OmniRoute/pull/13803)) — thanks @delafu
- **fix(translator):** Claude tool `input_schema` with a root-level `anyOf` / `oneOf` / `allOf` is flattened into a plain object schema instead of being forwarded verbatim. Anthropic refuses such a tool before inference (`tools.N.custom.input_schema: input_schema does not support oneOf, allOf, or anyOf at the top level`), so a single MCP/agent tool carrying one made every request fail with no combo failover possible ([#13552](https://github.com/diegosouzapw/OmniRoute/issues/13552)) ([#13561](https://github.com/diegosouzapw/OmniRoute/pull/13561)) — thanks @sprintberlin
- **fix(routing):** Preserve forced reasoning effort across native requests, account defaults and combo fallbacks while keeping internal routing directives out of upstream payloads. ([#13556](https://github.com/diegosouzapw/OmniRoute/pull/13556)) — thanks @JxnLexn
- **fix(providers):** MiniMax-M3's inline `<think>...</think>` reasoning no longer leaks into `message.content`/`delta.content` on the `minimax`/`minimax-cn` routes — it is now stripped and surfaced as `reasoning_content`, in both streaming and non-streaming responses (#13558) ([#13799](https://github.com/diegosouzapw/OmniRoute/pull/13799)) — thanks @pan17
- **fix(api):**`PATCH /api/settings` now persists `hideAutoCombos` and `hideNoThinkVariants` instead of silently dropping them (#13562) ([#13800](https://github.com/diegosouzapw/OmniRoute/pull/13800)) — thanks @texastoland
- fix(api): resolve the codex-settings `apiKey` through the canonical key resolver instead of an inline 400 guard, so the dashboard Apply flow no longer fails with `baseUrl, apiKey and model are required` in cloud mode when no management key is selected (#13563) ([#13566](https://github.com/diegosouzapw/OmniRoute/pull/13566)) — thanks @opensource-elearning
- fix(sse): release the native Codex turn pin when the pinned model becomes model-scoped unusable, so a long-running Codex session falls back to the next healthy combo model instead of dying to a terminal `400 NATIVE_CODEX_PINNED_MODEL_UNAVAILABLE` (#13564) ([#13566](https://github.com/diegosouzapw/OmniRoute/pull/13566)) — thanks @opensource-elearning
- **fix(proxy):** ordinary SOCKS5 data-plane requests no longer trigger the T14 speculative bare-TCP reachability probe — that probe opened and immediately closed a raw TCP connection, which a SOCKS5 listener (e.g. GOST) sees as an incomplete handshake and logs as `unexpected EOF`; HTTP/HTTPS fast-fail and the explicit `directFallbackOnUnreachable` control-plane probe are unchanged ([#13571](https://github.com/diegosouzapw/OmniRoute/pull/13571)) — thanks @mdigitalbh81
- **fix(gemini):** stop sending Gemini a `400` on combos ("function call turn comes immediately after a user turn") when conversation history *opens* on a functionCall turn — after `mergeConsecutiveSameRoleContents`, `contents[]` alternation is guaranteed for every index ≥ 1, but the one remaining violation was a history whose very first turn is a functionCall. Prepends a synthetic leading user turn in that case ([#13573](https://github.com/diegosouzapw/OmniRoute/pull/13573)). — thanks @hartmark
- fix(db): make the chat-path proxy resolver (`resolveProxyForConnection`) rotate a multi-member pool the same way the registry resolver already does — its per-connection cache was freezing on the first pool member forever instead of re-running the scope's round-robin/sticky/random strategy on each request, unless the connection needs a stable egress (opencode's egress-bucketed quota, grok-web's IP-pinned `cf_clearance`) (#13575) ([#14044](https://github.com/diegosouzapw/OmniRoute/pull/14044))
- **fix(proxies):** a subscription refresh, a bulk re-import or an API update that omits the status no longer turns a disabled proxy back on, and a refresh no longer rewrites a manual proxy that shares a subscription node's address ([#13577](https://github.com/diegosouzapw/OmniRoute/pull/13577)) — thanks @maxmad64bis
- **fix(cli):**`omniroute update` now passes `--legacy-peer-deps` to `npm install -g`, suppressing the `ERESOLVE` / peer-dependency wall seen on fresh global installs; dry-run output reflects the same flag; troubleshooting guide documents the supported install form (#13579 — thanks @prabhu-omkar)
- **fix(api):** a partial update no longer resets the fields the client did not send: renaming a disabled reasoning routing rule keeps it disabled with its priority, description and tags, renaming a playground preset keeps its params, and renaming or re-importing a proxy keeps its address family ([#13582](https://github.com/diegosouzapw/OmniRoute/pull/13582)) — thanks @maxmad64bis
- **fix(providers):** Antigravity error responses and logs now surface the real upstream message (e.g. Gemini field-path rejections) instead of the generic "Antigravity upstream error (400)" placeholder (#13591) ([#13801](https://github.com/diegosouzapw/OmniRoute/pull/13801)) — thanks @afonsoft
- **fix(usage):** the call-logs artifact worker's failure warning now includes the underlying error's message/code instead of the generic "detail omitted" — a crashed or non-zero-exit worker was previously undiagnosable in the logs (#13597) ([#13802](https://github.com/diegosouzapw/OmniRoute/pull/13802)) — thanks @afonsoft
- **fix(providers):** echo back `reasoning_content` on `bai` DeepSeek thinking-mode follow-up turns, fixing the upstream 400 "reasoning_content must be passed back" (#13599) ([#13807](https://github.com/diegosouzapw/OmniRoute/pull/13807)) — thanks @afonsoft
- **fix(i18n):** drop the second copy of `featureFlagProxySkipRecentlyFailedDescription` that the 2026-09-15 batch merges left in 59 dashboard catalogs (a scripted keep-both conflict resolution concatenated the key both PRs carried; `JSON.parse` silently kept the last copy) and the duplicated `ERROR_TYPE_CONTRACT` import in `src/lib/db/callLogStats.ts` (TS2300); adds `tests/unit/i18n-catalogs-no-duplicate-keys.test.ts`, a raw-text guard that fails on any key declared twice in one object of `src/i18n/messages/*.json` or `bin/cli/locales/*.json` ([#13602](https://github.com/diegosouzapw/OmniRoute/pull/13602), [#13641](https://github.com/diegosouzapw/OmniRoute/pull/13641)) ([#13816](https://github.com/diegosouzapw/OmniRoute/pull/13816))
- **fix(proxy):** proxy credentials holding a literal `%` (e.g. `pa%ss`) no longer break the proxy — HTTP(S) proxies now receive a correctly built `Proxy-Authorization` header instead of undici throwing `URIError`, SOCKS5 proxies get the raw credential, and the proxy registry, subscription import and legacy settings parsers keep the value instead of dropping the entry; correctly percent-encoded credentials decode exactly as before ([#13605](https://github.com/diegosouzapw/OmniRoute/pull/13605)) — thanks @maxmad64bis
- **fix(connection-cooldown):** skip connection cooldown for locally rejected token-budget 429s so a per-key limit never cools a healthy connection ([#13606](https://github.com/diegosouzapw/OmniRoute/pull/13606)) — thanks @maxmad64bis
- **fix(opencode-plugin-v2):** write the catalog snapshot to a temp file and rename it into place, ignore newer snapshot versions, and warn when a write is skipped or fails ([#13607](https://github.com/diegosouzapw/OmniRoute/pull/13607)) — thanks @maxmad64bis
- **fix(proxy-health):** a target-refused probe (401/403/429) can reset the consecutive-failure streak instead of staying neutral, behind the opt-in `PROXY_HEALTH_BLOCKED_RESETS_STREAK` feature flag (default off: refusals keep the #10654 neutral policy); a relayed 5xx stays inconclusive and a refusal never removes or disables a proxy ([#13608](https://github.com/diegosouzapw/OmniRoute/pull/13608)) — thanks @maxmad64bis
- **fix(providers):** opt-in `MISTRAL_AMBIGUOUS_401_SOFT_LOCKOUT` flag (default off): a bare Mistral 401 with no explicit auth signal (identical for a revoked key and an exhausted quota) cools the connection down instead of parking it as expired, at most 3 times per hour per connection before it parks, so a revoked key still converges; the ambiguity check is now one implementation shared by the connection test and the runtime ([#13609](https://github.com/diegosouzapw/OmniRoute/pull/13609)) — thanks @maxmad64bis
- **fix(proxies):** pool validation no longer rewrites proxies set to inactive or dead; only active and error statuses are updated ([#13612](https://github.com/diegosouzapw/OmniRoute/pull/13612)) — thanks @maxmad64bis
- **fix(opencode):** the v2 plugin reads the management token from OMNIROUTE_MANAGEMENT_API_KEY (plugin option wins) and warns once at startup when management calls fall back to the inference key ([#13613](https://github.com/diegosouzapw/OmniRoute/pull/13613)) — thanks @maxmad64bis
- **fix(routing):** a failed stale-pin (LKGP) clear on the combo fallback path now logs the combo and execution key while staying non-blocking, and a new opt-in `npm run check:routing-error-guard` script (not wired into CI) flags new swallowed catches and unanchored fire-and-forget async on routing paths, with frozen entries keyed by file and catch body instead of line numbers ([#13614](https://github.com/diegosouzapw/OmniRoute/pull/13614)) — thanks @maxmad64bis
- **fix(opencode):** opt-in `OPENCODE_TRANSIENT_FAILOVER_BACKOFF` flag (default off): once two consecutive opencode accounts fail with a transient upstream error, the rotation pauses before the next account (1.5 s doubling, capped at 6 s per pause and 10 s per request), releases the failed response body first and stops dispatching if the client disconnects during the pause; with the flag off failover stays immediate ([#13615](https://github.com/diegosouzapw/OmniRoute/pull/13615)) — thanks @maxmad64bis
- **fix(devin):** Fall back to the CLI probe (`devin acp --agent-type summarizer`) when the connection-test HTTP API rejects a CLI-format key, since routing authenticates against the local Devin CLI, not `api.devin.ai` ([#13617](https://github.com/diegosouzapw/OmniRoute/pull/13617)) — thanks @patrykkopycinski
- **fix(sse):** stream reasoning deltas from combo targets incrementally instead of buffering them into a single burst, and stop rejecting reasoning-only streams as an empty completion (#13620) ([#13806](https://github.com/diegosouzapw/OmniRoute/pull/13806)) — thanks @NaNomicon
- **fix(models):** keep OpenRouter's Batch-API-only `:batch` variants out of chat routing — ModelSync imported all 77 of them into the chat catalogue, where every request that landed on one was rejected with `404 This model is only available through the Batch API` (#13622) — thanks @L4XB
- **fix(cursor):**`kv_after_text` no longer settles away a trailing `exec_mcp` tool call in the same buffer, preventing Composer from dropping in-flight MCP tool invocations during KV checkpoint settling ([#13627](https://github.com/diegosouzapw/OmniRoute/pull/13627)) — thanks @patrykkopycinski
- fix(providers): restore grok-4.6/4.5 default reasoning effort so requests without an explicit effort keep reasoning enabled (#13628) — thanks @HouMinXi
- fix(registry): declare supportedThinkingEfforts on claude-opus-5 and claude-fable-5 across the anthropic/claude/claude-web/ghe-copilot/github registries (#13628) — thanks @HouMinXi
- **fix(stream-recovery):** opt-in `STREAM_RECOVERY_TOOLCALL_ORDER_FIX` (default off) makes mid-stream continuation tool-call safe — a cut stream is never resumed once a tool call was emitted, whether still in flight or already finished with `finish_reason: "tool_calls"` — and closes after one empty continuation instead of spending the whole budget ([#13633](https://github.com/diegosouzapw/OmniRoute/pull/13633)) — thanks @maxmad64bis
- **fix(ci):** clear the `release/v3.8.51` base-reds on the PR fast path. The provider pipeline keeps the upstream error code and type again, so an Antigravity missing-project 422 stays fail-closed. The glued-prefix `sk-` credential pattern scans error text in linear time instead of quadratic. The provider detail page no longer bundles `node:fs`. `/v1/models` stops listing custom Jina models twice. The `models`/`providers` import cycle is gone, and `opencode-plugin-v2` passes the pack policy. Stale guards, fixtures, locale keys and docs counts are aligned with their merged changes ([#13635](https://github.com/diegosouzapw/OmniRoute/pull/13635)) — thanks @dpozimski
- **fix(tests):** add `dist/httpClientAbortGuard.mjs` to the expected missing-paths list in `tests/unit/pack-artifact-policy.test.ts` — [#13636](https://github.com/diegosouzapw/OmniRoute/pull/13636) registered the file in `PACK_ARTIFACT_REQUIRED_PATHS` without updating the assertion, leaving the test red on the release tip for every PR that runs it ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13872](https://github.com/diegosouzapw/OmniRoute/pull/13872))
- **fix(compression):** report compression-worker faults instead of silently sending the uncompressed body, and fall back to the in-process pipeline for fast faults (thread error, exit, engine throw); a dispatch timeout still degrades to uncompressed, but is now logged ([#13637](https://github.com/diegosouzapw/OmniRoute/pull/13637)) — thanks @marcs7
- **fix(db):** search stats and analytics no longer surface "ghost" rows — a NULL/`-` provider or a keyed search provider whose connection was deleted — while keyless providers (`duckduckgo-free`, `searxng-search`, anonymous `context7`) and credential-fallback providers (`perplexity-search` on a `perplexity` key) stay visible; the analytics totals apply the same filter, so `total` always matches the per-provider breakdown ([#13641](https://github.com/diegosouzapw/OmniRoute/pull/13641)) — thanks @maxmad64bis
- **fix(sse):** recognize `reasoning_effort` in the reactive 400 field-strip retry — strict OpenAI-compatible upstreams that reject the field are retried once without it instead of surfacing the 400 ([#13642](https://github.com/diegosouzapw/OmniRoute/pull/13642)) — thanks @Moseyuh333
- **fix(dashboard):** new opt-in flag `FREE_BADGE_REQUIRES_PROVIDER_FREE_TIER` (default off) makes the provider-page Free badge strict — it drops the display-name heuristic, non-boolean `free` fields and `:free` suffixes on registered providers without a documented free tier, while keeping catalogued free models, explicit `free: true` and `:free` on free-tier providers and compatible nodes; with the flag off the badges are unchanged ([#13645](https://github.com/diegosouzapw/OmniRoute/pull/13645)) — thanks @maxmad64bis
- **fix(api):** share the single SOCKS5 flag reader across the settings proxy routes so the dashboard and the dispatcher stay consistent ([#13646](https://github.com/diegosouzapw/OmniRoute/pull/13646)) — thanks @maxmad64bis
- **fix(stream-recovery):** log every mid-stream continuation outcome with its `attempt N/MAX` token — the stitched suffix, overlap rejection, terminal/empty continuation and tool-call refusals at debug, and a recovery that gives up (budget spent, or the continuation request returned no stream) at warn — without adding any warn line to a healthy or tool-call stream; the existing `mid-stream continuation attempt N/MAX` line is unchanged ([#13650](https://github.com/diegosouzapw/OmniRoute/pull/13650)) — thanks @maxmad64bis
- **fix(sse):** Kiro translator no longer re-prepends the full relocated tool-documentation block onto every subsequent turn of a multi-turn conversation; it now stays anchored to the turn that originally carried it. (#13652) ([#13808](https://github.com/diegosouzapw/OmniRoute/pull/13808)) — thanks @KelvinKSPS
- **fix(sse):** opt-in `OPENCODE_RATE_LIMITED_429_EARLY_STOP` flag (default off): an opencode 429 classified as a real rate limit (parseable `Retry-After`, or a body naming a rate/usage limit) stops the cross-account wave and returns that upstream 429 unchanged — body, `Retry-After` and quota headers intact, so the opencode quota error rules still apply; unclassified 429s keep rotating, and with the flag off every 429 rotates as before (#9611) ([#13657](https://github.com/diegosouzapw/OmniRoute/pull/13657)) — thanks @maxmad64bis
- **fix(sse):** a configured daily-quota reset hour that falls inside a daylight-saving gap (New York 02:00 on spring-forward, Havana/Santiago midnight) now resolves to the first wall-clock time that exists instead of landing an hour early, sometimes on the previous day ([#13671](https://github.com/diegosouzapw/OmniRoute/pull/13671)) — thanks @maxmad64bis
- **fix(sse):** new opt-in flag `RETRY_AFTER_PROVENANCE_ENABLED` (default off): aggregated 429/503 unavailable responses omit `Retry-After` when no concrete future retry time is known instead of sending a synthetic 1s, carry `error.retry_after_provenance` (`signal` | `none`), and combo drain paths read prose retry hints from JSON and plain-text upstream bodies; non-JSON upstream error pages no longer log at warn ([#13672](https://github.com/diegosouzapw/OmniRoute/pull/13672)) — thanks @maxmad64bis
- **fix(docker):** isolate the ChatGPT Web (Codex) CDP proxy sidecar onto its own Compose network, add an opt-in `CDP_PROXY_TOKEN` auth gate to `cdp-proxy.mjs`, and stop the VNC browser-login CDP bridge from starting when no token is configured (#13679) ([#13811](https://github.com/diegosouzapw/OmniRoute/pull/13811))
- **fix(security):** the CLI/management bearer token is now derived from a random per-install salt persisted under `DATA_DIR` instead of the checked-in literal `omniroute-cli-auth-v1` — since `/etc/machine-id` is commonly world-readable, any local user could previously derive the same token as every install that never set `OMNIROUTE_CLI_SALT`; the explicit env override still takes priority and rotation still works the same way (#13679) ([#13909](https://github.com/diegosouzapw/OmniRoute/pull/13909))
- **fix(auth):**`verifyCloudSignature()` no longer accepts an unverifiable `X-Cloud-Sig` when `OMNIROUTE_CLOUD_SYNC_SECRET` is unset — a forged/garbage signature is rejected outright, and the new opt-in `OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE=true` flag rejects unsigned Cloud-sync payloads too (default stays legacy pass-through for v3.8.x; the default flips in v3.9) ([#13679](https://github.com/diegosouzapw/OmniRoute/issues/13679)) ([#13804](https://github.com/diegosouzapw/OmniRoute/pull/13804))
- **fix(security):** default the published Docker image and `fly.toml` deployment to `REQUIRE_API_KEY=true` (npm/CLI local-dev default unchanged), and stop `/api/free-tier/summary`'s wildcard CORS from leaking the operator's local token usage to unauthenticated callers (#13679) ([#13911](https://github.com/diegosouzapw/OmniRoute/pull/13911))
- **fix(security):** removed the copy-pasteable placeholder `JWT_SECRET`/`API_KEY_SECRET`/`INITIAL_PASSWORD` values from the Podman Quadlet deploy manifest, and blocked remote dashboard logins with the well-known default `INITIAL_PASSWORD=CHANGEME` (#13679) ([#13812](https://github.com/diegosouzapw/OmniRoute/pull/13812))
- **fix(security):** the internal self-loop admission-bypass bearer is now a random per-process secret instead of the checked-in literal `"sk_omniroute"` when no `OMNIROUTE_API_KEY`/`ROUTER_API_KEY` is configured (#13679) ([#13813](https://github.com/diegosouzapw/OmniRoute/pull/13813))
- **fix(db):**`DELETE /v1/batches/delete-completed` now caps the work it does per request and reports `hasMore` so a caller can resume, and the sweep no longer deletes a file that another batch still references (#13680, #13681) ([#13805](https://github.com/diegosouzapw/OmniRoute/pull/13805))
- **fix(security):** a restricted API key is now enforced on the alias spellings `next.config.mjs` rewrites onto `/api/v1/…` — a route handler sees the client's original URL, so `POST /chat/completions`, `/responses`, `/responses/*`, `/models`, `/codex/*` and the doubled `/v1/v1/*` prefix all skipped the endpoint-category lookup and let a key allowed only on `search` reach chat or any other endpoint ([#13685](https://github.com/diegosouzapw/OmniRoute/issues/13685)) ([#13741](https://github.com/diegosouzapw/OmniRoute/pull/13741)) — thanks @gonisulaimann
- **fix(usage):** mark locally estimated token usage in the call log (`_omniroute.usageEstimated` on the logged response) so operators can tell estimated counts and costs from provider-reported ones — covers OmniRoute's own estimate for streams without upstream usage and web executors that report `estimated: true`; billing, API-key budgets, quota-share and client payloads are unchanged ([#13686](https://github.com/diegosouzapw/OmniRoute/pull/13686)) — thanks @maxmad64bis
- **fix(openrouter):** sync the `:free` 1000/day tier from `/credits` lifetime purchases instead of staying stuck at 50/day for $10+ accounts ([#13689](https://github.com/diegosouzapw/OmniRoute/pull/13689)) — thanks @Notaloop763
- **fix(translator):** prevent schema property name collisions (e.g. `properties`, `required`) in Gemini schema sanitizer ([#13690](https://github.com/diegosouzapw/OmniRoute/pull/13690)) — thanks @zcrew0x
- fix(providers): map `thinking.type: "adaptive"` to `"enabled"` for AgentRouter GLM models instead of forwarding it unhandled, fixing a 400 from AgentRouter's upstream GLM endpoint (#13696) ([#14043](https://github.com/diegosouzapw/OmniRoute/pull/14043))
- **fix(providers):** the shared Responses-API input sanitizer now converts Codex's proprietary `agent_message` input items (used for multi-agent task/reply passing) into a plain `message` item before forwarding to any non-Codex-native Responses upstream. Previously such items reached third-party Responses endpoints untouched, and OpenCode Go Muse Spark 1.3 rejected the request with `input[N] did not match any supported type` (#13698). The real Codex/ChatGPT native passthrough path is unaffected and continues to receive `agent_message` items as-is. ([#14041](https://github.com/diegosouzapw/OmniRoute/pull/14041))
- fix(sse): stop the direct (no-proxy) fresh-socket retry from reusing the pooled attempt's flat `OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS` response-start watchdog — the retry is a brand-new socket with no zombie to detect (#10214's rationale only applies to the pooled attempt), so when the caller already attached its own deadline signal (the resolved connection/model/provider/`FETCH_TIMEOUT_MS` cascade) the retry now defers to a generous, `OMNIROUTE_DIRECT_RESPONSE_RETRY_TIMEOUT_MS`-configurable backstop instead of an identical short flat window, fixing spurious 504s on healthy slow-TTFB reasoning models (#13703) ([#14047](https://github.com/diegosouzapw/OmniRoute/pull/14047))
- **fix(copilot):** fall back to the `copilot-chat` identity once when a standard GitHub Copilot account rejects the CLI identity with a 403, without breaking Enterprise Copilot ([#13705](https://github.com/diegosouzapw/OmniRoute/pull/13705)) — thanks @tuandinh0801
- **fix(dashboard):** Saving or clearing a proxy on a provider page now refreshes the per-connection proxy badges immediately instead of leaving them stale until a manual reload ([#13711](https://github.com/diegosouzapw/OmniRoute/pull/13711)) — thanks @xiaoyaner0201
- fix(db): bound health scans and isolate native diagnostics so large quota histories no longer exhaust memory or block request handling; diagnostics now run in a cancellable child and are awaited at HTTP and MCP callers (#13717) — thanks @HouMinXi
- fix(chat): preserve suffix-model reasoning effort across model attempts so a replacement model no longer inherits or drops the original suffix, and keep explicit reasoning choices in request dedup hashes (#13720) — thanks @HouMinXi
- **fix(api):**`POST /v1/rerank` now actually works against native TEI / Infinity provider nodes: the `/rerank` fallback sends `texts` + `return_text` alongside `documents`, and bare-array or `score`-only upstream responses are normalized to the Cohere `{results: [{index, relevance_score, document?}]}` envelope (sorted, `top_n`-capped) so clients and the memory engine's rerank step see real scores ([#13733](https://github.com/diegosouzapw/OmniRoute/pull/13733)) — thanks @seanford
- **fix(api):**`GET /v1/models` types compatible-provider-node rows by the node's `apiType` when a discovered/added model carries no endpoint metadata — an `embeddings` node's models are `type: "embedding"` and a `rerank` node's models are `type: "rerank"` instead of surfacing as untyped chat models; a manual overlay's `supportedEndpoints` also re-types the merged row ([#13734](https://github.com/diegosouzapw/OmniRoute/pull/13734)) ([#13740](https://github.com/diegosouzapw/OmniRoute/pull/13740)) — thanks @seanford
- **fix(memory):** the Embedding and Rerank selectors on Memory → Engine now list local provider nodes with the models they actually expose for that modality (synced + custom rows, typed the same way `GET /v1/models` types them) instead of by node `apiType` alone with an empty model list — a node typed `embeddings` that also serves a reranker now appears under Rerank with that model, and under Embeddings with its embedding model, so `prefix/model` no longer has to be typed by hand ([#13740](https://github.com/diegosouzapw/OmniRoute/pull/13740)) — thanks @seanford
- **fix(db):** Health-check-repair backup pruning now resolves `maxFiles`/`retentionDays` from the persisted Storage-page setting (not just env vars), matching manual/API/auto backups. ([#13308](https://github.com/diegosouzapw/OmniRoute/issues/13308)) ([#13773](https://github.com/diegosouzapw/OmniRoute/pull/13773)) — thanks @voidstackloop
- **fix(sse):** CCR retrieve-tool detection now recognizes an MCP-gateway-namespaced tool name (e.g. Docker MCP Toolkit's `mcp__docker__omniroute__omniroute_ccr_retrieve` or a single `mcp__<server>__omniroute_ccr_retrieve` prefix) via a separator-bounded trailing-segment match instead of requiring an exact `omniroute_ccr_retrieve` string — previously an MCP-capable caller reachable only under a gateway-assigned name was treated as unable to retrieve, so the entire CCR compression engine was skipped for it (#13781). ([#14028](https://github.com/diegosouzapw/OmniRoute/pull/14028))
- **fix(i18n):** retranslate the English strings that had been copied verbatim into the locale catalogs (Spanish alone carried 7,142) and turn the real-translation ratio gate into a blocking ratchet. (#13782)
- fix(resilience): the chat admission gate's pressure check now actively re-samples instead of reading a passive cache, so the `resource_pressure` guard can observe recovery and stop shedding once real pressure clears, instead of requiring a full process restart ([#13823](https://github.com/diegosouzapw/OmniRoute/pull/13823)) — thanks @pandudpn
- **fix(providers):** the Zylo API key check now probes the authenticated chat route instead of the open catalog — Zylo serves `GET /v1/models` without authentication, so the account-setup dialog accepted any string as valid and the key was only rejected later, when a model test returned Zylo's own `401 "Key not found: zk-…"` ([#13828](https://github.com/diegosouzapw/OmniRoute/issues/13828)) ([#13877](https://github.com/diegosouzapw/OmniRoute/pull/13877))
- **fix(sse):** when a gateway API key's `allowed_connections` / quota scope hides every connection of a provider, chat now answers `403` naming that scope instead of the generic `No active credentials for provider: X` — which was indistinguishable from "never configured" even though `/test` and `/sync-models` kept working on the same connection ([#13832](https://github.com/diegosouzapw/OmniRoute/issues/13832)) ([#13879](https://github.com/diegosouzapw/OmniRoute/pull/13879))
- **fix(combos):** Keep vision capability consistent for MiMo V2.5 and Step 3.7 Flash provider/free variants so `/v1/combos` no longer under-reports multimodal combos whose members are already advertised as vision-capable by `/v1/models` ([#13847](https://github.com/diegosouzapw/OmniRoute/issues/13847)). ([#13863](https://github.com/diegosouzapw/OmniRoute/pull/13863)) — thanks @smshagor-dev
- **fix(translator):** Third-party tool names (e.g. GitHub Copilot's own `web_fetch` function tool) are no longer sent unprefixed to Claude-wire-format providers outside genuine first-party Anthropic traffic, fixing a `rejected tool(s): web_fetch` 400 for any `gh/claude-*` model ([#13856](https://github.com/diegosouzapw/OmniRoute/pull/13856)) — thanks @dylanhaskins
- fix(build): externalize @modelcontextprotocol/sdk in standalone server webpack config to prevent TDZ ReferenceError on MCP initialize (#13859) — thanks @HouMinXi
- fix(combo): stop the chars/4 context estimate from demoting an operator-verified `model_context_override` behind an unconfirmed catalog "emergency" fallback in combo priority ordering (#13870) ([#14046](https://github.com/diegosouzapw/OmniRoute/pull/14046))
- fix(quota): resolve Quota Sharing plan/limits from the pool's canonical primary connection in `enforceQuotaShare` and `recordConsumption`, not the serving connection — a multi-connection pool whose "Limite" wizard override only ever lands on the primary connection was writing/checking consumption under a different dimension key when a request was actually served via a non-primary pool member, so the dashboard's "consumed" amount never reflected real traffic (#13876) ([#14042](https://github.com/diegosouzapw/OmniRoute/pull/14042))
- Fixed a security issue where a revoked, expired, or banned API key could still resolve an owner scope in `getApiKeyRequestScope()` and keep accessing its own `/v1/files` and `/v1/batches` records instead of being rejected with 401 (#13881). ([#14024](https://github.com/diegosouzapw/OmniRoute/pull/14024))
- fix(security): scope `/api/files` and `/api/batches` management siblings to the caller's own API key (session stays instance-wide), closing a cross-tenant read that let an anonymous or foreign-key caller enumerate and download other tenants' files/batches (#13882) ([#14027](https://github.com/diegosouzapw/OmniRoute/pull/14027))
- **security(images):** close a DNS-rebinding TOCTOU (#13883) at the three newer public-only image download sites — `resolveImageSource` and the NanoBanana result-URL conversion in `imageGeneration.ts`, and `resolveUpscaleImageSource` in `imageUpscale/shared.ts`. All three validated a caller-supplied URL's DNS answer as public but then let the download perform an independent, un-pinned second resolution at connect time, so a host that answered differently between the two lookups (public, then loopback/LAN) could reach an internal address; they now set `pinDns: true` (reusing the existing `createPinnedFetch` helper already used by embeddings and the vision/audio/video bridges), binding the connection to the exact validated address. ([#14032](https://github.com/diegosouzapw/OmniRoute/pull/14032))
- **fix(i18n):** reviewer pass over the 1,865 pt-BR leaves retranslated in #13782 (172 corrections) via the new `review-locale` script. (#13885)
- **fix(db):**`getPricingForModel()` now reads through the existing 30s TTL `getCachedPricing()` helper instead of rebuilding pricing from scratch (3 SELECTs + JSON.parse + merge) on every call, eliminating the multi-second event-loop stall `/api/usage/history` hit when `calculateAggregateCost()` invoked it once per GROUP BY row (up to 531 times per request) (#13891). Every known pricing writer already invalidates this cache via `touchPricing()`, so writes remain immediately visible. ([#14040](https://github.com/diegosouzapw/OmniRoute/pull/14040))
- **fix(cli):** translate the CLI for every locale — 38 catalogs carried 1–24 of 830 keys and fell back to English; `sync-ui-keys --catalog=cli` fills them (52,000 strings) and a completeness gate keeps them full. (#13892)
- **fix(dashboard):** the request-log detail view now shows an explicit "payload omitted — exceeded the call log size limit" notice for a pipeline/request/response section that was replaced by the size-limit marker (`_omniroute_truncated` / `[omitted: call log artifact size limit exceeded]`), instead of silently rendering the marker verbatim under a generic "Pipeline Error" title as if it were a real upstream error. Also fixed `.env.example` documenting stale `CHAT_LOG_ARRAY_TAIL_ITEMS=128`/`CHAT_LOG_MAX_DEPTH=6` defaults that no longer match the code's actual `1000`/`20`. ([#14045](https://github.com/diegosouzapw/OmniRoute/pull/14045))
- **fix(sse):** the CCR protocol instruction (the system note teaching a model how to call `omniroute_ccr_retrieve`) is now injected for callers reaching OmniRoute's MCP server through a namespacing gateway (Claude Code / Docker MCP style `mcp__<gateway>__<server>__omniroute_ccr_retrieve`, or a dotted/slashed prefix) — `callerSupportsCcrRetrieve()` previously matched by exact string equality only, so a genuinely reachable but gateway-namespaced tool name was never recognized and the instruction (and CCR compression itself) was silently skipped (#13897). ([#14028](https://github.com/diegosouzapw/OmniRoute/pull/14028))
- fix(tests): replace the flaky 250ms wall-clock ReDoS guard in `sanitizeErrorMessage`'s property test with a deterministic cost-scaling check, so the test proves bounded-backtracking instead of failing on machine load (#13907) ([#14039](https://github.com/diegosouzapw/OmniRoute/pull/14039))
- fix(providers): `detectVisionInput` now recognizes Lemonade Server's `labels[]` vision capability, so Lemonade vision models import with `supportsVision` set instead of being treated as text-only (#13918) ([#14023](https://github.com/diegosouzapw/OmniRoute/pull/14023))
- fix(sse): deepseek provider registry now declares `defaultContextLength: 1_000_000` and an explicit `deepseek-flash` model entry, so unlisted/new DeepSeek models (like the new DeepSeek V4.1 Flash) no longer fall back to the generic 128k context limit (#13922) ([#14026](https://github.com/diegosouzapw/OmniRoute/pull/14026))
- **fix(sse):** opt-in `OPENCODE_PARK_AND_RESUME` flag (default off): after repeated transient 429s the opencode rotation parks the request with a heartbeat and replays one capped leg of up to 3 sequential accounts instead of fanning out the whole fleet; with the flag off every 429 rotates as before ([#13924](https://github.com/diegosouzapw/OmniRoute/pull/13924)) — thanks @maxmad64bis
- fix(guardrails): resolve nested `combo-ref` steps to their real leaf models when deciding vision-bridge behavior, so a pass-through combo pointing at an all-vision-capable inner combo skips the describe-and-replace path instead of stripping raw images (#13927) ([#14038](https://github.com/diegosouzapw/OmniRoute/pull/14038))
- **fix(docs):** re-sync the 65 documentation mirror sets — 817 mirrors rewritten: the 14 core sources edited since their translation, the 322 mirrors that were still English copies, and the frontmatter the old extractor had leaked into the newer locales' bodies. The docs pipeline now retranslates only the `## ` sections whose text changed, and the drift gate is blocking. (#13940)
- fix(combos): synchronize allowedProviders and allow invariant override when updating combos from dashboard ([#13951](https://github.com/diegosouzapw/OmniRoute/pull/13951)) — thanks @fouadSalkini
- **fix(i18n):** translate the 3,719 `__MISSING__` markers (61 keys × 61 locales) that eight base PRs stamped into the catalogs on 2026-09-16, restoring the real-translation ratio gate on the release tip. (#13974)
- **fix(providers):** OpenRouter model discovery honors the per-connection base URL override instead of always importing the global catalog, so a connection pointed at a regional endpoint (e.g. the EU in-region host) no longer advertises model ids that endpoint cannot serve ([#14001](https://github.com/diegosouzapw/OmniRoute/pull/14001)) — thanks @tiangao88
- **fix(providers):** Perplexity Web no longer collapses runs of spaces in non-streaming answers (the path tool mode always takes), which flattened code indentation in `write_file` arguments and plain code blocks; citation markers are still removed with single spacing left behind ([#13968](https://github.com/diegosouzapw/OmniRoute/issues/13968)) ([#14009](https://github.com/diegosouzapw/OmniRoute/pull/14009)) — thanks @costajohnt
- **fix(docker):** copy the app into the runtime image with `--chown=node:node` instead of a second `chown -R` layer, so the image no longer stores the ~2 GB standalone build twice ([#13990](https://github.com/diegosouzapw/OmniRoute/issues/13990)) ([#14010](https://github.com/diegosouzapw/OmniRoute/pull/14010)) — thanks @costajohnt
- **fix(opencode):** an OpenCode free-tier refusal no longer counts as a healthy response and no longer takes the account it landed on out of rotation for that model: the 403 is recorded on the connection instead of staying unclassified, it stops clearing the refused account's failure history, and the request comes back without a pointless hop across accounts that would all get the same verdict. Every sibling account returns the same answer to the same request, so one refusal per account would otherwise empty the pool and leave later requests answered "no active credentials" ([#14011](https://github.com/diegosouzapw/OmniRoute/pull/14011)) — thanks @maxmad64bis
- **fix(opencode):** keyless OpenCode models answer again instead of returning `403` — requests now carry a versioned OpenCode user-agent, canonical session and request ids derived from the existing conversation fingerprint, a streamed upstream request, and a tool list. The upstream inspects which tool names a request declares, so rather than pinning a list, OmniRoute reuses the one a request of the same conversation was last seen getting through: a title or a summary, which its client sends without tools, goes out with the list that client already declared. Two opt-outs (`OPENCODE_FREE_TIER_REQUEST_CONTRACT`, `OPENCODE_FREE_TIER_PLACEHOLDER_TOOLS`) ([#14013](https://github.com/diegosouzapw/OmniRoute/pull/14013)) — thanks @maxmad64bis (with thanks to @AStupidBear for the identity-header work in #13937)
- **fix(build):** drop the orphaned `httpClientAbortGuard.mjs` entries from the pack-artifact allowlists — the #13636 crash-guard wiring was removed, so no producer or consumer ships the file anymore ([#14029](https://github.com/diegosouzapw/OmniRoute/pull/14029)) — thanks @maxmad64bis
- **feat(sse):** multi-account rotation now spreads sends per network egress and eases off fleet-wide when throttled, opt-in via `OPENCODE_EGRESS_THROTTLE_ENABLED=1` ([#14290](https://github.com/diegosouzapw/OmniRoute/pull/14290)) — thanks @maxmad64bis
- Fix Antigravity image generation not rotating to another account when the upstream returns an explicit quota-exhausted 429, so a second configured account with available quota is no longer stuck behind the first account's terminal quota error. ([#9908](https://github.com/diegosouzapw/OmniRoute/pull/9908)) — thanks @Ardem2025
- **fix(sse):**`requestQueue.maxQueueDepth = 0` (the documented default, "disabled") once again means an unbounded account queue. #12911 redefined `0` inside `accountSemaphore` as "reject when busy" for its Codex WS leases, and because `chatCore` forwards `maxQueueDepth` straight into that option, every request that found its account slot busy under default settings was answered 429 `Semaphore queue full (0)` instead of waiting. The lease keeps its refuse-don't-wait behaviour through an explicit `failFast` option. ([#14101](https://github.com/diegosouzapw/OmniRoute/pull/14101))
- **fix(sse):** the streaming OpenAI→Claude translator relays reasoning for legacy callers that never pass `requestedThinking` (`undefined`), matching the non-streaming path and the pre-#12905 contract; only an explicit opt-out (`false`) suppresses it, and only that case synthesizes the reasoning into a text block. ([#14101](https://github.com/diegosouzapw/OmniRoute/pull/14101))
- **fix(security):**`requestRejectedFailure.ts` (#12864) sanitizes the upstream message at its own `lastError` writes instead of trusting the caller to have done so, and the public-boundary guard now covers the extracted module. ([#14101](https://github.com/diegosouzapw/OmniRoute/pull/14101))
- **fix(adobe-firefly):** never spawn a real Chrome for CDP session warm under a unit-test runner, so tests stop leaking a browser process that holds an OS handle on its DATA_DIR profile directory ([#13289](https://github.com/diegosouzapw/OmniRoute/pull/13289)) — thanks @anhtahaylove
- fix(providers): **declare Agnes CN chat models' live `reasoning_effort` vocabulary** so the catalog and sanitizer stop inventing tiers the CN API rejects. Probes on api.agnes-ai.cn (2026-09-14) match the international endpoint: 2.0/2.5 accept `none/low/medium/high/max`, 3.0 also accepts `minimal` and `xhigh`; `off`/`ultra` clamp off the wire and Hermes' default `xhigh` clamps to `max` on 2.x. ([#13399](https://github.com/diegosouzapw/OmniRoute/pull/13399)) — thanks @HouMinXi
- fix(providers): **declare Agnes chat models' live `reasoning_effort` vocabulary so catalog/builder/sanitizer stop inventing aliases the API 400s.** 2.0/2.5 accept `none/low/medium/high/max`; 3.0 also accepts `minimal` and `xhigh`. `off`/`ultra` still clamp off the wire. ([#13655](https://github.com/diegosouzapw/OmniRoute/pull/13655)) — thanks @HouMinXi
- Fix Antigravity quota parsing treating an unreported `remainingFraction` as 0% remaining instead of unknown, which made a genuinely exhausted quota indistinguishable from one the upstream simply didn't report. ([#7138](https://github.com/diegosouzapw/OmniRoute/pull/7138)) — thanks @Ardem2025
- **fix(api):** accept `blockedModels` in the key permissions update schema so the deny-list half of per-key model policy is no longer silently stripped before it reaches the route ([#13666](https://github.com/diegosouzapw/OmniRoute/pull/13666)) — thanks @fouadSalkini
- **fix(tests):** bump the `APIKEY_PROVIDERS` tripwire count to 241 — Agnes AI China (#13399) added one `apikey/regional` entry, and the stale 240 was failing a unit-test shard on every open PR against `release/v3.8.51` ([#13905](https://github.com/diegosouzapw/OmniRoute/pull/13905))
- **fix(ci):** the advisory `forgotten-sibling-tests` step no longer fails "Fast Quality Gates" when a PR touches a hub module — the cross-product of consumers × candidate tests reached millions of rows and rendering them exceeded V8's maximum string length, so the throw hit `main()`'s catch and exited 1. The report now lists at most 200 rows per section (and 5 000 per array in the JSON artifact) while the header keeps the exact totals ([#13889](https://github.com/diegosouzapw/OmniRoute/pull/13889))
- **fix(compression):** Caveman's `leader_phrases` rule (and any other anchored file-pack rule) works again. #12825 replaced the keyword prefilter for file-based packs with a test of the rule's own regex, but ran it against a lower-cased snapshot of the _original_ text — so `^(?:i will|…)` never matched once the leading `Sure, ` was still there, and the rule was skipped before it could see the text pleasantries had already cleaned. The prefilter now sees the text as earlier rules left it. ([#14164](https://github.com/diegosouzapw/OmniRoute/pull/14164)) — thanks @gonisulaimann / @prabhtheone / @xiaoyaner0201 / @xiechimon
- **Dashboard:** renamed the Claude connection field helpers to `claudeConnectionFieldValues.ts` — the `.ts`/`.tsx` pair from #13074 differed only by casing, which breaks webpack on case-insensitive filesystems and made esbuild resolve the wrong module in the browser-bundle guard. ([#14164](https://github.com/diegosouzapw/OmniRoute/pull/14164)) — thanks @gonisulaimann / @prabhtheone / @xiaoyaner0201 / @xiechimon
- **fix(oauth):** stop posting a Claude refresh token that another in-process refresh already consumed. Re-check the rotation map and DB inside `serializeRefresh` (both Layer 1 and Layer 2), record Layer 2 rotations, re-read the connection uncached on `invalid_grant`, and keep the Claude `refreshToken` instead of nulling it into sticky `no_refresh_token`. ([#13874](https://github.com/diegosouzapw/OmniRoute/pull/13874)) — thanks @ai-jeremi-esky
- **fix(oauth):** stop nulling the Claude refresh token on the first unrecoverable refresh failure so `CredentialHealth` no longer gets stuck sticky-dead — the retry budget from #11414 can now actually spend its second attempt instead of finding an already-cleared token (#13183) ([#13185](https://github.com/diegosouzapw/OmniRoute/pull/13185)) — thanks @RaviTharuma
- **fix(stream):** restore Claude SSE passthrough `tool_use` names to the casing the client actually declared instead of "upgrading" them to the canonical Claude Code spelling (`bash` → `Bash`), which broke third-party Anthropic-format clients (pi/OpenCode on claude-format executors like devin-cli-agentic) while leaving the JSON path correct; the pre-existing Claude Code protection (upstream downcase restored to declared PascalCase, #7926) is preserved ([#12855](https://github.com/diegosouzapw/OmniRoute/pull/12855)) — thanks @Neuron-Mr-White
- **fix(oauth):** classify embedded `invalid_grant` in Cline token refresh error bodies so permanently consumed refresh tokens trigger re-authentication instead of indefinite transient retry loops, and add `cline` to `ROTATION_LOCK_GROUP` to serialize concurrent sibling refreshes ([#13466](https://github.com/diegosouzapw/OmniRoute/pull/13466)) — thanks @fouadSalkini
- Add a visible manual callback-entry action to the Codex loopback warning so remote users can paste the authorization result instead of setting up an SSH tunnel. ([#9944](https://github.com/diegosouzapw/OmniRoute/pull/9944)) — thanks @Ardem2025
- Fixed Codex executor forwarding client `reasoning` sub-fields (`enabled`, `max_tokens`, `exclude`) that the Codex Responses API rejects with HTTP 400, taking down every combo target with a deterministic client error. The reasoning object is now whitelisted to `effort`/`summary`, and `enabled: false` maps to effort `none` when no more specific effort was requested. ([#13643](https://github.com/diegosouzapw/OmniRoute/pull/13643)) — thanks @HouMinXi
- **fix(combo):** retry the same target once when a streaming response fails before any content reaches the client (`streaming upstream error`), including native-pinned Codex turns whose set retries stay disabled — previously the caller returned a 502 immediately instead of using the existing transient-retry loop ([#13630](https://github.com/diegosouzapw/OmniRoute/pull/13630)) — thanks @anhtahaylove
- **Emergency fallback:** the budget-exhaustion reroute targets `nvidia/openai/gpt-oss-120b` again, as documented in `ENVIRONMENT.md` and the NVIDIA hosted-model snapshot; #14006 had switched the provider to `groq` inside an unrelated MITM change, so operators without a Groq connection got the original 402 back. ([#14164](https://github.com/diegosouzapw/OmniRoute/pull/14164)) — thanks @gonisulaimann / @prabhtheone / @xiaoyaner0201 / @xiechimon
- **fix(security):** client-supplied image URLs (`image_url` / `mask_url` / message parts on image generation and upscale, chat `image_url` parts inlined by the vision bridge) and the NanoBanana result download now pin the `public-only` outbound guard with DNS validation, instead of inheriting the operator provider policy — `block-metadata` on a default install let a request body make the server fetch loopback/LAN URLs and forward the bytes upstream (GHSA-34rg-3pqj-35g9) ([#13748](https://github.com/diegosouzapw/OmniRoute/pull/13748))
- **fix(authz):** classify the 14 remaining spawn-capable `/api/cli-tools/*` routes (`all-statuses`, `status`, `detect` and the `claude/cline/codewhale/codex/crush/deepseek-tui/droid/kilo/openclaw/pi/smelt-settings` writers) and the `/api/skills/install` + `/api/skills/executions` pair as LOCAL_ONLY — they reach `child_process.spawn` transitively (`getCliRuntimeStatus()` / `detectAllTools()` / the skills sandbox) but only sat behind Tier 3 MANAGEMENT auth, which `requireLogin=false` waives; loopback/LAN enforcement now runs before any auth check, matching their already-gated siblings (GHSA-35fw-cv32-2373 — thanks Parth Narula; GHSA-jx89-f37j-pq89 — thanks Aeon). Tunnel-served dashboards lose the CLI Tools status badges, the same trade-off already accepted for grok/forge/jcode/qwen. ([#13745](https://github.com/diegosouzapw/OmniRoute/pull/13745))
- **fix(security):** redact Groq (`gsk_…`), xAI (`xai-…`) and every OpenAI-compatible `sk-…` key shape (DeepSeek 32-hex, Moonshot/Kimi, Together, …) in error bodies and the opt-in credential-masker guardrail — the catalog only knew the exact 48-char OpenAI form, so those keys passed through the guardrail verbatim and `gsk_`/`xai-` also reached public error responses (GHSA-r4q7-7f24-m29p) ([#13744](https://github.com/diegosouzapw/OmniRoute/pull/13744))
- **fix(grok-cli):** a 429 "used all the included free usage … rolling 24-hour window" on Grok Build is quota exhaustion for that model, not a 30s rate-limit wait. Combo skips the drained grok-4.6 login and tries the next account ([#13984](https://github.com/diegosouzapw/OmniRoute/pull/13984)) — thanks @stormsia
- **Rate limit:**`requestQueue.maxWaitMs=0` (the disable sentinel from #12902) no longer trips the #12715 queue-budget gate — every request on a protected connection was rejected with an immediate `503 queue budget` instead of waiting without a queue deadline. Execution stays bounded by `executionMaxWaitMs` and the upstream fetch-start timeout. ([#14164](https://github.com/diegosouzapw/OmniRoute/pull/14164)) — thanks @gonisulaimann / @prabhtheone / @xiaoyaner0201 / @xiechimon
- **fix(mcp):** MCP audit treats a non-callable better-sqlite3 export (`better-sqlite3 export is not a function`) as a native load failure, falls back to `node:sqlite`, and caches a failed driver load so dashboard polls stop reprinting (a database file that does not exist yet is never cached, so the connection recovers once the app creates it). Docker now refuses to ship without `better_sqlite3.node`. Native-load classification lives in `sqliteLoadError.ts` so `core.ts` stays under its frozen line cap. The build bootstrap keeps a deliberately narrower classifier: a corrupt binding there must not be read as "no encrypted credentials", or a fresh `STORAGE_ENCRYPTION_KEY` would be generated over an existing encrypted database. ([#13903](https://github.com/diegosouzapw/OmniRoute/pull/13903)) — thanks @HouMinXi
- **fix(api):** the model-test skip for image/music/video-only models (#13376) returned a result with no `httpStatus`, and the `/api/models/test` route hands that field straight to `NextResponse` — so a skipped test reached the client as HTTP 200 carrying `status: "error"` in the body. It now answers 422: the request is valid, but that model's modality cannot be exercised by a chat test. Also clears the `TS2741` that was failing `API Route Typecheck` on the release branch. ([#13730](https://github.com/diegosouzapw/OmniRoute/pull/13730))
- **fix(sse):**`detectMalformedNonStream` no longer flags a Claude-format message as malformed when the content array only contains empty text blocks or the `(empty response)` sentinel and `stop_reason` is `"length"` — a legitimate truncation (e.g. `ollama/qwen3:1.7b` exhausting its reasoning budget before producing visible text), not a real 502-worthy empty response. ([#12935](https://github.com/diegosouzapw/OmniRoute/pull/12935)) — thanks @jasminsehic
- **fix(compression):** output styles and the caveman output mode now place their injected instruction in the top-level `system` field instead of a synthetic `messages[0]` entry for Anthropic-shaped requests, fixing the upstream 400 ("use the top-level 'system' parameter for the initial system prompt") ([#12584](https://github.com/diegosouzapw/OmniRoute/issues/12584)) ([#13383](https://github.com/diegosouzapw/OmniRoute/pull/13383)) — thanks @Xore
- Add a shadow release-acceptance report next to release-green.json. It does not close #12732 and is not a Mergify required check. ([#13701](https://github.com/diegosouzapw/OmniRoute/pull/13701)) — thanks @HouMinXi
- **fix(sse):** chat admission PSI uses the container cgroup `memory.pressure` file instead of host-wide `/proc/pressure/memory`, so a swapping host no longer 503s an idle Docker/cgroup OmniRoute with `resource_pressure`; the host file remains the fallback when the cgroup sample is missing ([#12562](https://github.com/diegosouzapw/OmniRoute/pull/12562)) — thanks @SCys
- **fix(responses):** ensure full compliance with the OpenAI Responses API streaming schema for strict deserializers (e.g. OpenAI Responses SDK, Grok CLI / pager): - Include `output: []`, `background: false`, and `error: null` in the `response.in_progress` lifecycle event across both the Responses transformer and response translator. - Include `status` (`in_progress` or `completed`) on all emitted output items (`message`, `reasoning`, `function_call`, `custom_tool_call`) in `response.output_item.added`, `response.output_item.done`, and `response.output[]`. - Include `sequence_number: 0` in in-band Responses stream error frames (`OPENAI_RESPONSES_ERROR_FRAME` and `buildResponsesErrorDataLine`) emitted after early keepalive streams commit. - Ensure `input_tokens_details` (with `cached_tokens: 0`) and `output_tokens_details` (with `reasoning_tokens: 0`) are always populated in `response.usage` even when upstreams (e.g. Gemini) omit reasoning or caching tokens. ([#13956](https://github.com/diegosouzapw/OmniRoute/pull/13956)) — thanks @TheDemonTuan
- **responses:** Responses-to-Chat fallback now skips replayed `web_search_call` metadata while preserving the paired function result, preventing deterministic HTTP 400 failures on follow-up turns routed to Chat Completions providers (#13304 ([#14090](https://github.com/diegosouzapw/OmniRoute/pull/14090)) — thanks @anhtran-ai)
- **fix(security):** bump the `adm-zip` override to `^0.6.1` — 0.6.0 followed a symlink already present inside the extraction root and could write outside it (GHSA-vwc7-r8mq-g2x9 / CVE-2026-76845); 0.6.1 walks every path component with `lstat` and refuses symlinks. Reached only through `onnxruntime-node`'s install script, which unpacks the vendor's own binary — no request-path exposure. ([#13737](https://github.com/diegosouzapw/OmniRoute/pull/13737))
- **fix(ci):** register the four unit tests whose mutant kills were not counting — `daily-reset-tz-threading`, `noauth-model-lockout`, `free-badge-provider-gate` and `local-token-budget-429-skips-cooldown` — in `stryker.conf.json``tap.testFiles`. `check-mutation-test-coverage --strict` reported 6 missing coverings across 4 mutated modules (`accountFallback`, `sse/services/auth`, `combo/comboPredicates`, `combo/rrState`) on the release line itself with no PR diff involved, so the `mutation-test-coverage` gate was red on every open PR regardless of its contents; the gate now reports no drift ([#13814](https://github.com/diegosouzapw/OmniRoute/pull/13814)) — thanks @abhisheksharma2411
- fix(ci): drop a `tap.testFiles` entry naming a deleted test file, and guard the direction the existing drift check never covered — an entry left behind after its test is removed or renamed costs mutation coverage silently, because Stryker resolves the list into its sandbox without failing on a dangling path ([#13814](https://github.com/diegosouzapw/OmniRoute/pull/13814)) — thanks @abhisheksharma2411
- fix(providers): give the TinyCMS wasm-bindgen Node DOM stub a dedicated `window` with a Location-shaped object (never `window = global` without `location`), so Next.js SSR `getLocationOrigin` cannot crash every route after TinyCMS is used once ([#13957](https://github.com/diegosouzapw/OmniRoute/pull/13957)) — thanks @aref-alapour
- **fix(analytics):** resolve account email/name in Utilization Account Split chart and fix legend bleeding through tooltip ([#13029](https://github.com/diegosouzapw/OmniRoute/pull/13029)) — thanks @ZaimMarzuki
- Electron release: `electron/package-lock.json` regained the optional `electron-builder-squirrel-windows` subtree (13 entries) that `npm ci` had been refusing as out of sync — the Linux desktop leg died on it — and `electron-release.yml` gained a `build_ref` dispatch input so a release whose tag was cut with the broken lock can have its assets rebuilt from the repaired line ([#13140](https://github.com/diegosouzapw/OmniRoute/pull/13140)) — thanks @ozeas
- npm publish workflow: the CycloneDX SBOM is attached to the GitHub Release on `workflow_dispatch` publishes too (when a release for the tag exists), not only on the `release` event — v3.8.50 shipped through a staged dispatch and its release carried no SBOM until it was attached by hand from the run's `sbom-npm` artifact ([#13140](https://github.com/diegosouzapw/OmniRoute/pull/13140)) — thanks @ozeas
- **fix(providers):** strip `<script>`/`<style>` blocks from the Vertex model-docs HTML even when the end tag carries junk before the `>` (`</script foo>`, which the HTML spec still treats as a close). The old regexp required `</script\s*>`, so such a block survived; the generic tag-stripping pass then removed both tags and kept the script BODY, letting its text reach the table cells the context-window/token-limit parser reads (CodeQL `js/bad-tag-filter`, alert #1007). ([#13936](https://github.com/diegosouzapw/OmniRoute/pull/13936))
- **fix(vertex):** prefer live model discovery for OAuth, Service Account, and service-account-bound authorization-key credentials while falling back cleanly to the Gemini-only Express catalog for standard API keys. Model Garden resources now route by publisher protocol: Claude and Mistral use their native `rawPredict` APIs, and Grok plus current or future open MaaS publishers use Vertex's OpenAI-compatible endpoint with normalized request IDs such as `xai/grok-4.6`. Distinguish intentional API-key catalog rejection from transient HTTP or network failures. ([#12471](https://github.com/diegosouzapw/OmniRoute/pull/12471)) — thanks @JxnLexn
- **fix(db):** remove the periodic `wal_checkpoint(TRUNCATE)` scheduler. Truncating the WAL rewrites the shared wal-index (`storage.sqlite-shm`) while other connections and in-flight statements still hold it mapped, which crashed long-running servers with SIGBUS roughly every six hours (#13973). WAL hygiene is unchanged: PASSIVE checkpoints still run every five minutes, now count busy contention and retry after 60s instead of silently logging, and a WAL above `OMNIROUTE_WAL_GUARD_MAX_MB` runs `wal_checkpoint(RESTART)` so the file stays bounded without rewriting the mapped index. The shutdown checkpoint still truncates. `OMNIROUTE_WAL_TRUNCATE_INTERVAL_MS` is ignored and logs a deprecation warning. ([#14005](https://github.com/diegosouzapw/OmniRoute/pull/14005)) — thanks @HouMinXi
- **fix(perplexity-web):** preserve system contract on follow-up requests ([#12443](https://github.com/diegosouzapw/OmniRoute/pull/12443)) — thanks @tanveer-arch
- **fix(translator):** map Claude stop_sequences and stop to Gemini stopSequences ([#12785](https://github.com/diegosouzapw/OmniRoute/pull/12785)) — thanks @Siva010
- **fix(cursor):** preserve native Claude effort model IDs before executor dispatch ([#12838](https://github.com/diegosouzapw/OmniRoute/pull/12838)) — thanks @Pllutonyy
- **fix(sse):** guard empty tool_calls[] and strip tool_choice without tools ([#12901](https://github.com/diegosouzapw/OmniRoute/pull/12901)) — thanks @initguru
- **fix(resilience):** allow maxWaitMs=0 as disable sentinel for execution expiration ([#12902](https://github.com/diegosouzapw/OmniRoute/pull/12902)) — thanks @initguru
- **fix(sse):** demote mid-conversation system roles to user in claude-to-openai translation ([#12908](https://github.com/diegosouzapw/OmniRoute/pull/12908)) — thanks @initguru
- **fix(soniox):** pass client parameters through and surface speaker diarization ([#12948](https://github.com/diegosouzapw/OmniRoute/pull/12948)) — thanks @amirrezakm
- **fix(tests):** drain call-log saves before chat-pipeline DB resets (#12780) ([#12966](https://github.com/diegosouzapw/OmniRoute/pull/12966)) — thanks @visheshgubrani
- **fix(providers):** resolve unsupportedParams via model aliases so K3 stops 400ing on temperature ([#13037](https://github.com/diegosouzapw/OmniRoute/pull/13037)) — thanks @patrykkopycinski
- **fix(db):** gate the auto-cleanup VACUUM on reclaimable space, not row count ([#13079](https://github.com/diegosouzapw/OmniRoute/pull/13079)) — thanks @hartmark
- **fix(logs):** recover concatenated JSON objects in the Provider Event Stream viewer ([#13115](https://github.com/diegosouzapw/OmniRoute/pull/13115)) — thanks @hartmark
- **fix(quota):** apply the equal-split fallback in the pool usage snapshot ([#13159](https://github.com/diegosouzapw/OmniRoute/pull/13159)) — thanks @datrixlab
- **fix(deps):** declare remark-gfm as a direct dependency ([#13162](https://github.com/diegosouzapw/OmniRoute/pull/13162)) — thanks @marioschoenert-code
- **fix(security,resilience):** block origin-IP header forwarding and treat 413 as retryable TPM ([#13350](https://github.com/diegosouzapw/OmniRoute/pull/13350)) — thanks @themedexperiencesusa
- **fix(build):** copy ioredis and bcryptjs into the standalone bundle ([#13352](https://github.com/diegosouzapw/OmniRoute/pull/13352)) — thanks @sistemabritto
- **fix(embeddings):** send stored API key on private-host embeddings nodes ([#13398](https://github.com/diegosouzapw/OmniRoute/pull/13398)) — thanks @HouMinXi
- **fix(claude-web):** add charset=utf-8 to Content-Type headers to fix Arabic/Persian UTF-8 mojibake (#13416) ([#13419](https://github.com/diegosouzapw/OmniRoute/pull/13419)) — thanks @KooshaPari
- **fix(compression):** track only the recursion path in isStrictlySerializable (#13154) ([#13423](https://github.com/diegosouzapw/OmniRoute/pull/13423)) — thanks @KooshaPari
- **fix(gemini):** send thinkingLevel for 3.8 Flash so thoughts stop eating the output cap ([#13463](https://github.com/diegosouzapw/OmniRoute/pull/13463)) — thanks @HouMinXi
- **fix(sse):** strip trailing assistant prefill on official Claude OAuth ([#13572](https://github.com/diegosouzapw/OmniRoute/pull/13572)) — thanks @HouMinXi
- **fix(providers):** clamp SenseNova DeepSeek V4 Flash effort to high ([#13626](https://github.com/diegosouzapw/OmniRoute/pull/13626)) — thanks @HouMinXi
- **fix(routing):** skip redundant parseAutoPrefix for recognized built-in auto variants ([#13647](https://github.com/diegosouzapw/OmniRoute/pull/13647)) — thanks @hummern
- **fix(embeddings):** log server-side when a provider can't be resolved ([#13687](https://github.com/diegosouzapw/OmniRoute/pull/13687)) — thanks @hartmark
- **fix(codex):** forward the caller client version upstream instead of a pinned default ([#13708](https://github.com/diegosouzapw/OmniRoute/pull/13708)) — thanks @zeeshanhaque21
- **fix(gemini):** a tool name starting with a digit no longer fails the request ([#13738](https://github.com/diegosouzapw/OmniRoute/pull/13738)) — thanks @L4XB
- **fix(admission):** measure request bodies with the active cost budget ([#13762](https://github.com/diegosouzapw/OmniRoute/pull/13762)) — thanks @lorenzozanee
- **fix:** match compatible-provider models owned by public prefix ([#13831](https://github.com/diegosouzapw/OmniRoute/pull/13831)) — thanks @sahildaswani
- **fix(ci):** drain three base reds blocking every PR — generated SKILL.md, stryker registry, env/docs contract ([#13834](https://github.com/diegosouzapw/OmniRoute/pull/13834))
- **fix(docs):** restore the env/docs contract broken by the #13679 vars ([#13875](https://github.com/diegosouzapw/OmniRoute/pull/13875))
- **fix(chatCore):** only preserve tool_result blocks for Claude-native targets ([#13972](https://github.com/diegosouzapw/OmniRoute/pull/13972)) — thanks @phs1997
- **fix(windows):** hide supervised server console ([#13992](https://github.com/diegosouzapw/OmniRoute/pull/13992)) — thanks @prabhtheone
- **fix(ci):** document OMNIROUTE_STRIP_SYSTEM_PREAMBLE — the env/docs base red blocking every PR ([#14022](https://github.com/diegosouzapw/OmniRoute/pull/14022))
- **fix(resilience):** extend process crash guard to combo hedge cancels and upstream fetch failures (#13636) ([#14064](https://github.com/diegosouzapw/OmniRoute/pull/14064)) — thanks @HouMinXi
- **fix(codex):** whitelist reasoning object keys before the wire (#13643) ([#14065](https://github.com/diegosouzapw/OmniRoute/pull/14065)) — thanks @HouMinXi
- **fix(sse):** aggregate the `findInsensitive` collision warnings into one line per catalog build instead of one per colliding key ([#12972](https://github.com/diegosouzapw/OmniRoute/pull/12972)) — thanks @IAMBOBJIM
### 📝 Maintenance
@@ -1502,62 +1083,6 @@ _By commits in `091589089c..06f1df9d77`, author identities consolidated via `.ma
- **chore(deps):** bump hono from ^4.12.34 to ^4.13.7 (#13148) ([#13301](https://github.com/diegosouzapw/OmniRoute/pull/13301)) — thanks @KooshaPari
- **chore(deps):** pin joi to ^18.2.8 via overrides (#13085) ([#13302](https://github.com/diegosouzapw/OmniRoute/pull/13302)) — thanks @KooshaPari
- `Coverage` job on `ci.yml`: the informational Codecov upload gets its own 5-minute ceiling and `continue-on-error`, and the job budget grows from 20 to 30 minutes (the 8-shard c8 merge alone takes ~10) — a stalled upload no longer ends the job `cancelled` and drags a fully green `main` run's conclusion down with it ([#11972](https://github.com/diegosouzapw/OmniRoute/pull/11972))
- **docs(dependencies):** clarify that `socket.yml` only shapes Socket.dev's registry-side post-publish scan of the published npm artifact — it is not an enforced CI/PR merge gate ([#12664](https://github.com/diegosouzapw/OmniRoute/pull/12664) — thanks @toor11).
- **chore(skills):** regenerate the `omni-settings` agent skill after the pool egress-observation route landed (#13581), clearing the `check:agent-skills-sync` base-red (#12732) ([#13826](https://github.com/diegosouzapw/OmniRoute/pull/13826))
- **test(call-logs):** the early-keepalive merge and video-bridge redaction tests pass a `traceId` (defaulting to `pendingRequestId`) now that #13546 keys each attempt's call-log row on it, the dashboard `request.failed` redaction probe reads the persisted row by `traceId`, and the keepalive test polls against a 30s wall-clock deadline like the video-bridge test instead of a 2.4s try count ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13747](https://github.com/diegosouzapw/OmniRoute/pull/13747))
- **fix(db):** drop the duplicated `ERROR_TYPE_CONTRACT` import in `src/lib/db/callLogStats.ts` left by the #13641 merge; the TS2300 duplicate-identifier error failed the API-route and dashboard typecheck gates on every PR ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13747](https://github.com/diegosouzapw/OmniRoute/pull/13747))
- **fix(cli):** add the `serve.ready_timeout` string to the `en`, `zh-CN` and `zh-TW` CLI catalogs; `--ready-timeout` shipped calling `t("serve.ready_timeout")` without a catalog entry, which the CLI i18n key-coverage and parity tests report ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13747](https://github.com/diegosouzapw/OmniRoute/pull/13747))
- **docs(env):** document `OMNIROUTE_SYNCED_CATALOG_STALE_AFTER_MS` (#12849, default 30 days) in `.env.example` and `ENVIRONMENT.md`; the stale-synced-catalog fail-open shipped the override without either, which the env/docs contract gate reports as code-only ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13678](https://github.com/diegosouzapw/OmniRoute/pull/13678))
- **fix(ci):** re-freeze `tests/unit/image-generation-handler.test.ts` (2133→2235, #13748) and `tests/unit/batch_api.test.ts` (1345→1348, #13749) at their merged size; PR-mode `check:file-size` does not relax `testFrozen` against the base, so that regression coverage turned the gate red on every PR into the release line ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13747](https://github.com/diegosouzapw/OmniRoute/pull/13747))
- **fix(ci):** allowlist the Uzbek `outputTokenDesc` translation ("Yakunlash/javob tokenlari") and the `PROTECTED_PRIORITY_INFRA_502_ENABLED` feature-flag id (#13439) in `.gitleaks.toml`; the `generic-api-key` rule reads the `...TokenDesc` key and the flag `key:` as token assignments, which the secrets ratchet reported as new findings ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13747](https://github.com/diegosouzapw/OmniRoute/pull/13747))
- **test(models):** the custom Jina specialty-model catalog test expects the `jina-ai/` prefix again: custom rows keep the connection provider id, only synced rows resolve through the `jina` alias, and #13403 had switched the custom assertion to `jina/` ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13747](https://github.com/diegosouzapw/OmniRoute/pull/13747))
- **chore(build):** ship `httpClientAbortGuard.mjs` in the published tarball — the #13636 crash guard was a new `server-ws.mjs` import missing from both pack-artifact allowlists (#12732) ([#13826](https://github.com/diegosouzapw/OmniRoute/pull/13826))
- **test(settings):** the #6540 paid-target tests now use `gemini/gemini-3.1-pro-preview` as the paid fixture and assert the fixtures still classify as paid/free/unknown; the old Together target became "unknown" once #13407 removed Together's one-time signup credit from the free catalog, so the three save-time blocking tests read a correct 200 as a missing guard ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13747](https://github.com/diegosouzapw/OmniRoute/pull/13747))
- **fix(ci):** the release-green validator now runs its package-artifact gate the way `ci.yml` does — build, stamp `dist/BUILD_SHA`, then validate against the tree under test. `build:cli` never writes the stamp, so even with the provenance ref pointed at `HEAD` the gate could only ever report "dist/BUILD_SHA is missing" once the build itself compiled ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13678](https://github.com/diegosouzapw/OmniRoute/pull/13678))
- **test(resilience):** the `/api/resilience` configuration-only key-set assertion now lists `credentialHealthCheck`, the sweep-interval setting #12043 added to the projection, so the integration suite stops reading a documented configuration key as leaked runtime state ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13678](https://github.com/diegosouzapw/OmniRoute/pull/13678))
- **fix(ci):** register `noauth-model-lockout`, `local-token-budget-429-skips-cooldown`, `free-badge-provider-gate` (#13645) and `daily-reset-tz-threading` (#13440) in `stryker.conf.json``tap.testFiles`; they cover `accountFallback.ts`/`auth.ts`/`comboPredicates.ts`/`rrState.ts`, so the strict `mutation-test-coverage` gate failed Fast Quality Gates on every PR into the release line ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13747](https://github.com/diegosouzapw/OmniRoute/pull/13747))
- **chore(ci):** register the #13609 ambiguous-401 regression test in the mutation-coverage config, clearing the second `check:agent-skills-sync`/`mutation-test-coverage` base-red (#12732) ([#13826](https://github.com/diegosouzapw/OmniRoute/pull/13826))
- **fix(tests):** eight combo integration suites still targeted the retired `claude-3-5-sonnet-20241022`, which the lifecycle registry rejects with 410; they now use its successor `claude-sonnet-4-6`, un-hiding 23 routing cases ([#13056](https://github.com/diegosouzapw/OmniRoute/pull/13056)) — thanks @doramirdor
- **test(coverage):** clean stale `coverage/` output before `test:coverage` runs, stopping unbounded accumulation of c8 raw snapshots and reports ([#13408](https://github.com/diegosouzapw/OmniRoute/pull/13408)) — thanks @MumuTW
- **docs(resilience):** correct `requestQueue.maxWaitMs` in the resilience guide and the environment reference — it bounds **queue wait**, not limiter-managed execution (that is `executionMaxWaitMs`), and the env vars only supply defaults that a persisted or per-connection value overrides (#13624) — thanks @L4XB
- **test(batches):** the two seeded-batch labels of the delete-completed route-scope suite that sat right after a `key*.id` argument are renamed to short literals (`route401`/`route500`), so a gitleaks scan that reads those lines (full-tree, or git-mode on a branch that adds them) no longer reports them as `generic-api-key` hits ([#13729](https://github.com/diegosouzapw/OmniRoute/pull/13729)) — no gate changes: the CI secret ratchet scans `src`/`open-sse`/`bin`/`electron`/`scripts`, never `tests/`
- **credit:** three squash merges on `release/v3.8.51` dropped the contributor attribution the review pipeline had preserved on the branches — the dual-layer semantic cache ([#14159](https://github.com/diegosouzapw/OmniRoute/pull/14159), re-land of [#12630](https://github.com/diegosouzapw/OmniRoute/pull/12630)) is @BillyOutlast's work, the native Codex auto-resume fix ([#14162](https://github.com/diegosouzapw/OmniRoute/pull/14162), re-land of [#13180](https://github.com/diegosouzapw/OmniRoute/pull/13180)) is @mdigitalbh81's, and the `resolvedExtensionEnd` reorder in [#13295](https://github.com/diegosouzapw/OmniRoute/pull/13295) landed first in @ggiak's [#13036](https://github.com/diegosouzapw/OmniRoute/pull/13036). This entry and its commit trailers record that credit ([#14361](https://github.com/diegosouzapw/OmniRoute/pull/14361)) — thanks @BillyOutlast, @mdigitalbh81 and @ggiak
- **chore(quality):** clear the `release/v3.8.51` base-reds — 19 failing unit tests plus the `API Route Typecheck` and `mutation-test-coverage` gates. Three fixtures still built `*-compatible-*` connections with no `baseUrl` and so tripped the #13452/#13798 guard that now refuses to fall back to the real OpenAI/Anthropic API; `modelDiscovery.ts` missed the `VertexModelMetadataProvenance` cast its read-path twin already had (#12471); a raw NUL byte in a provider-test regexp made git treat the file as binary; and the reserved-prefix count, the budget-card SVG and the Stryker `tap.testFiles` list had drifted. The #2331, OAuth-loopback and i18n guards were re-expressed as the invariants they protect — each re-verified by mutating the source back and watching it fail. ([#13947](https://github.com/diegosouzapw/OmniRoute/pull/13947))
- **ci:** repair the `API Route Typecheck` base-red on `release/v3.8.51` — type the awaited-callback contract of `runWithConnectionFetch` (glmResetCards), annotate `handleSingleModelChat(): Promise<Response>`, and give the codex-responses-ws bridge helpers real `{ error } | payload` discriminants so `"error" in x` narrows again; baseline ratcheted 294 → 283 (no widening). ([#14079](https://github.com/diegosouzapw/OmniRoute/pull/14079))
- **quality:** rebaseline `open-sse/executors/codex.ts` 1552 → 1553 — the +1 landed with #14065 (#13643) without a baseline entry and turned `check:file-size` red on the release tip.
- **compression/tests:** repair the unit base-reds on `release/v3.8.51` left by the 09-17 merge wave — RTK dedup was skipped for every unknown-type text (#13521 widened `skipFilters` to `isDocumentLikeRead`), so repeated tool output stopped compressing (12 tests); the translate-path golden, the `APIKEY_PROVIDERS` count and the G13 golden/SSE `Content-Type` assertions were stale after #12648 (xKiro) and #13419 (`charset=utf-8`). ([#14082](https://github.com/diegosouzapw/OmniRoute/pull/14082))
- **quality:** owner-approved file-size rebaseline for the 2026-09-18 merge-train 8 (32 contributor PRs whose irreducible growth lands in already-frozen files — 28 ceilings raised to the measured combined sizes; per-PR attribution in `config/quality/file-size-baseline.json`).
- **chore(quality):** adjust the train-8 accountFallback ceiling to the re-measured 2517 (direct commit `271ec25f12`)
- **test(usage):** pin fetcherProviders against supportedProviders ([#13134](https://github.com/diegosouzapw/OmniRoute/pull/13134)) — thanks @abhisheksharma2411
- **chore:** add Windows helpers to run OmniRoute and Claude Code from a source checkout ([#13312](https://github.com/diegosouzapw/OmniRoute/pull/13312)) — thanks @easypathuni
- **test:** realign two stale assertions with product behavior (#13313) ([#13315](https://github.com/diegosouzapw/OmniRoute/pull/13315)) — thanks @anhtahaylove
- **chore(stryker):** register 3 covering unit tests missing from tap.testFiles ([#13357](https://github.com/diegosouzapw/OmniRoute/pull/13357)) — thanks @patrykkopycinski
- **test(adobe-firefly):** lock in the browser-spawn guard with a regression test ([#13358](https://github.com/diegosouzapw/OmniRoute/pull/13358)) — thanks @patrykkopycinski
- **chore(deps):** bump better-sqlite3 to ^13.0.3 ([#14049](https://github.com/diegosouzapw/OmniRoute/pull/14049)) — thanks @Hakarioz
- **ci(acceptance):** emit a shadow release-acceptance report next to release-green (#13701) ([#14066](https://github.com/diegosouzapw/OmniRoute/pull/14066)) — thanks @HouMinXi
- **docs(i18n):** refresh the mirrors and keys the base left behind; warn on stale mirrors ([#14166](https://github.com/diegosouzapw/OmniRoute/pull/14166))
- **deps:** 4 Dependabot bumps — bump oven/bun from 1.4.0-slim to 1.4.2-slim ([#12977](https://github.com/diegosouzapw/OmniRoute/pull/12977)); bump js-yaml ([#13212](https://github.com/diegosouzapw/OmniRoute/pull/13212)); bump the development group across 1 directory with 15 updates ([#13661](https://github.com/diegosouzapw/OmniRoute/pull/13661)); bump electron from 44.0.0 to 44.3.0 in /electron ([#13664](https://github.com/diegosouzapw/OmniRoute/pull/13664))
### 🙌 Contributors
@@ -1566,235 +1091,120 @@ Thanks to everyone whose work landed in v3.8.51:
_Living section — regenerated 2026-08-12 from all cycle commits (cycle open `ed2db6cb19` → tip). Bullets carry the merged PR and its author; direct pushes listed separately._
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 360 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 360 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 54 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 359 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 359 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 54 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
<br/>
<br/>
@@ -486,7 +486,7 @@ All **19** strategies — mix & match per combo step:
</div>
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 360 providers, 150+ free tiers built in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA and 42 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 359 providers, 150+ free tiers built in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA and 42 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
<sub>📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
@@ -862,7 +862,7 @@ with a scoped access token; every command then targets the remote.
```bash
omniroute connect 192.168.0.15 # password → scoped token, saved as a context
omniroute models # ← runs against the REMOTE server
omniroute models list# ← runs against the REMOTE server
omniroute configure codex # ← picks a remote model, writes a local Codex profile
omniroute tokens create --name ci --scope read# mint narrower tokens for other machines
omniroute contexts use default # ← switch back to the local server
@@ -1006,10 +1006,6 @@ omniroute
```
> 💡 See `npm warn ERESOLVE` or peer-dep warnings? [They're harmless](docs/guides/TROUBLESHOOTING.md#npm-install-warnings-eresolve--peer--deprecated).
> **Using Gemini Web or another web-cookie provider?** The npm package includes
- **feat(docs):** every Markdown page under `docs/` is now mirrored in all 65 dashboard locales, not only the 22-page core set — 152 sources × 65 locales = 9,880 mirrors (6,208 new), with the 🌐 language bar of every mirror rewritten for the full locale list. The docs drift gate (`npm run i18n:check`, blocking in CI) derives its scope from the tree, so it now guards all 152 pages. Found and fixed by the run in `scripts/i18n/run-translation.mjs`: a markdown table or tight bullet list with no blank line inside it (PROVIDER_REFERENCE.md's 244-row table, FREE_TIERS.md's 71-item list) was sent as one 16–40 KB request that outlived the backend socket for verbose scripts (Greek, Amharic); oversized runs of table rows or list items are now cut at item boundaries and rejoined without a blank line, so no chunk exceeds 6 KB across the docs tree. 48 older mirrors whose tables had lost rows were retranslated with the fixed chunker.
- **feat(usage):** `openai-compatible-*` connections can now report billing/quota in Provider Limits. The connection declares its own quota endpoint, auth mode and a dot-path mapping onto `UsageQuota` in `providerSpecificData.quotaEndpoint`, so no upstream-specific code is needed per service — a mapping that resolves nothing reports no quota rather than an exhausted-looking 0/0 ([#13616](https://github.com/diegosouzapw/OmniRoute/issues/13616))
- **feat(sse):** track LLM Gateway DevPass quota — the `llmgateway` provider now reads its monthly plan-credit and weekly premium-model allowance from `GET /v1/key` and surfaces both windows in Dashboard › Limits and quota-aware preflight ([#12462](https://github.com/diegosouzapw/OmniRoute/pull/12462)).
- **feat(providers):** add Lyceum (lyceum.technology) as an OpenAI-compatible, pay-per-use provider — chat, embeddings, and live `/models` discovery through `https://api.lyceum.technology/openai/v1`, plus a credit-balance quota fetcher (`GET /api/v2/external/billing/credits`) surfaced in Dashboard › Limits and quota-aware preflight ([#12470](https://github.com/diegosouzapw/OmniRoute/pull/12470)).
- **feat(proxylogs):** proxy log columns and detail pane now show the registry proxy name instead of a bare `host:port` when several registry entries share the same gateway ([#12814](https://github.com/diegosouzapw/OmniRoute/pull/12814)) — thanks @tiangao88
- **feat(codex):** safely discover compatible models by classifying upstream models before activation to keep hidden, unsupported, retired, or newer-client models out of the active catalog, exposing candidate diagnostics while persisting only active models, adding GPT-6 Astra fallback definitions, and bumping the tested Codex CLI version to 0.153.4 ([#12933](https://github.com/diegosouzapw/OmniRoute/pull/12933)) — thanks @TheDemonTuan
- **feat(api):** `POST /api/keys` accepts `expiresAt` (ISO datetime, nullable) with the same semantics as the key-update path, so automation can create an expiring key in one operation instead of create-then-update. Omitted/null preserves the current non-expiring behavior; enforcement reuses the existing expiry policy ([#12952](https://github.com/diegosouzapw/OmniRoute/pull/12952)) — thanks @caniko
- **feat(sse):** `OMNIROUTE_DISABLE_CONVERSATION_TRACKING=1` turns off conversation-history collection for operators who do not use the dashboard's conversation view. `resolveConversationId()` returns an untracked result before it reads SQLite or parses message history, and the switch also covers client-supplied session IDs. Routing-session handling is unchanged, tracking stays on by default, and existing records are not deleted. One reporting install held 5.97 million turn records at about 4.26 GB ([#13150](https://github.com/diegosouzapw/OmniRoute/pull/13150))
- **feat(providers):** Add `auto/kimi`, `auto/qwen`, `auto/deepseek`, `auto/gpt`, and the `auto/claude-haiku` fast variant to the built-in routing catalog, including bare `k3` models on Kimi coding and web backends (issue #13214).
- **feat(usage):** Claude OAuth usage now shows the separate weekly Fable limit next to the shared five-hour and weekly meters. Anthropic reports it as a `weekly_scoped` entry in `limits[]`, which OmniRoute ignored, so the pool was invisible. The provider-limits cache keeps `modelQuotas` and restores it on stale-data fallback. The Fable meter is display-only and does not affect routing, account selection, or cooldowns ([#13266](https://github.com/diegosouzapw/OmniRoute/pull/13266))
- **feat(reasoning):** adaptive reasoning effort (`auto`) — the gateway resolves the thinking budget per user turn from deterministic request-shape signals (stateless per-turn pin) instead of forwarding a literal `auto`, applied at the gateway pre-translation for any harness (Claude Code, Cursor, Codex, opencode, Hermes) whose request dispatches to an OpenAI Chat-Completions-shaped upstream (`targetFormat === FORMATS.OPENAI` — `reasoning_effort` is an OpenAI-shaped field, so a Claude- or Gemini-targeted request is unaffected). Opt in via `X-OmniRoute-Effort: auto` or a model's `defaultReasoningEffort: "auto"` (now a valid `ModelSpec` value); any explicit client reasoning field always wins ([#13448](https://github.com/diegosouzapw/OmniRoute/pull/13448))
- **feat(proxies):** proxy pools and opencode's per-account rotation stop re-serving a proxy that just failed (refused TCP probe, or a 429 through it) for a period that doubles on each repeat up to a cap, without writing any proxy status; with every candidate set aside the choice is unchanged. Opt-in via the `PROXY_SKIP_RECENTLY_FAILED` feature flag (default off: selection unchanged) ([#13578](https://github.com/diegosouzapw/OmniRoute/pull/13578)) — thanks @maxmad64bis
- **feat(proxy-logs):** proxy log rows keep the HTTP status the provider actually returned (`upstream_status`, null when no response arrived), so a throttled egress IP (429), a refused one (403) and a provider outage (500) are no longer the same "error" line, and a 429 generated locally is no longer mistaken for one from the provider ([#13580](https://github.com/diegosouzapw/OmniRoute/pull/13580)) — thanks @maxmad64bis
- **feat(proxies):** the proxy pool editor shows, for the last 24 h, how many distinct egress IPs actually served the pool's members, how many connections went through them and the most seen behind one IP, read from the proxy log through a separate route so it can never break the pool screen; opt-in with the `PROXY_POOL_EGRESS_OBSERVATION` feature flag (default off) ([#13581](https://github.com/diegosouzapw/OmniRoute/pull/13581)) — thanks @maxmad64bis
- **feat(sse): learn hard request caps stated in 429 bodies and pace under them.** Providers such as TokenRouter reject bursts with prose like `Maximum 5 requests within 1 minutes` and no rate-limit headers, so the limiter never learned the ceiling and kept racing into it; every 429 also tore the limiter down and rebuilt it with no pacing. `updateFromResponseBody` now parses that phrasing (and `N requests per minute`, `N requests per M seconds`, `N RPM`) into a per-window cap, applies it to the limiter as an empty reservoir that refills `N` every window with calls spread `window / N` apart, and records it in `learnedRateLimits`. A learned cap is reapplied whenever the limiter is rebuilt after a 429 and when limits are restored at startup, unless the connection has an explicit RPM override. Fixes [#13594](https://github.com/diegosouzapw/OmniRoute/issues/13594).
- **feat(proxies):** a proxy pool stops re-serving a member the provider just refused through it and tries another member instead, reusing the existing skip cooldown; a later success through the member clears it. Opt-in with the `PROXY_SKIP_RECENTLY_FAILED` feature flag (default off: pool selection unchanged) ([#13602](https://github.com/diegosouzapw/OmniRoute/pull/13602)) — thanks @maxmad64bis
- **feat(flags):** add `DB_HEALTHCHECK_STARTUP_DEFERRED_ENABLED` (default off) — opt-in deferral of the startup DB health/integrity check past process boot via `setImmediate`; off keeps the pre-#13717 behavior of blocking `getDbInstance()` until the check has already run (#13717).
- **feat(i18n):** 7 new locales — Hausa (`ha`), Yoruba (`yo`), Igbo (`ig`), Amharic (`am`), Uzbek (`uz`), Georgian (`ka`), Armenian (`hy`) — across the dashboard, docs mirrors, CLI, README and the site (66 locales, the full planned expansion from 43). (#13727)
- **feat(api):** `POST /v1/rerank` (and the memory engine's loopback rerank step) can route to OpenAI-compatible provider nodes on a LAN/Tailscale host — not only loopback — behind the new `RERANK_REMOTE_PROVIDER_NODES` feature flag (default off), subject to the provider outbound URL policy; the loopback host check is consolidated into `@/shared/network/loopbackNodeHost` shared by rerank, audio, and the local health checker ([#13732](https://github.com/diegosouzapw/OmniRoute/pull/13732)) — thanks @seanford
- **feat(security):** OmniRoute now warns at boot when the server that answers `/v1` inference is bound to a non-loopback interface while `REQUIRE_API_KEY` is disabled. The guard added in [#12568](https://github.com/diegosouzapw/OmniRoute/pull/12568) covered the API bridge (`API_HOST`, default loopback) and the live dashboard WebSocket, but not the Next server that actually serves `/v1/chat/completions` and `/v1/responses` — which binds `HOST || 0.0.0.0`, every interface by default. That matters because `GET /v1/models` follows the dashboard login posture (`requireAuthForModels`) while inference follows `REQUIRE_API_KEY`, so an instance with an admin password and `REQUIRE_API_KEY=false` answers `401` to the probe an operator naturally runs while inference stays open to anyone who can reach the port. The bound host is resolved from `OMNIROUTE_BOUND_HOST` (published by `scripts/dev/run-next.mjs`) then Next's own `HOSTNAME` (the Docker path); `HOST` is deliberately excluded because the standalone server ignores it and a warning naming the wrong interface is worse than none. New `docs/security/INFERENCE_AUTH_POSTURE.md` documents the split, how to actually probe inference, and the [#2257](https://github.com/diegosouzapw/OmniRoute/issues/2257) caveat that an invalid bearer degrades to anonymous. ([#13820](https://github.com/diegosouzapw/OmniRoute/pull/13820))
- Expose combo wall-clock timeout (`comboTimeoutMs`) next to Target timeout in the combo editor and Combo defaults. Empty keeps the 10-minute hang-stop; a positive value replaces it. ([#13857](https://github.com/diegosouzapw/OmniRoute/pull/13857))
- **feat(i18n):** `retranslate-site` rewrites the site catalogs' verbatim-English leaves (2,059 across 63 catalogs; mean English residue 10.3 % → 6.3 %, the rest being brand names kept on purpose). (#13886)
- **feat(compression):** Lite tool-result truncation length is configurable (`lite.maxToolLength`, env `OMNIROUTE_LITE_MAX_TOOL_LENGTH`). Default stays 2000. An out-of-range step cap no longer hides a valid global cap; a toggle-only settings write keeps a stored cap; `maxToolLength: null` clears it. Dashboard copy no longer hard-codes 2,000 characters. ([#13915](https://github.com/diegosouzapw/OmniRoute/pull/13915) — refs [#13178](https://github.com/diegosouzapw/OmniRoute/issues/13178))
- **feat(proxy):** support multiple local core endpoints, one per line ([#13923](https://github.com/diegosouzapw/OmniRoute/pull/13923) — thanks @maxmad64bis)
- **feat(kiro):** expose Kiro's provider-native Opus 5 Max effort tier — `<base>-max` in the Claude effort catalog, `max` in the Kiro effort values, and the adaptive-thinking envelope for `claude-opus-5` ([#14284](https://github.com/diegosouzapw/OmniRoute/pull/14284)) — original change by tarciorick, thanks @bufftop25
- **feat(sse):** forward the auto mode classifier beta (`dangerous-tool-use-2026-09-03`) to Anthropic-format upstreams, and let `anthropic-compatible-*` providers forward client-negotiated betas at all, so Claude Code sessions behind the gateway stay eligible for server-side auto mode classification ([#14312](https://github.com/diegosouzapw/OmniRoute/pull/14312)) — thanks @dpozimski
- feat(providers): **Added Agnes AI (China) as `agnes-cn` pointed at `https://api.agnes-ai.cn/v1`. Keys issued for `apihub.agnes-ai.com` stay on the existing `agnes` card. Live `/v1/models` on that host lists `agnes-3.0-flash` (same id as intl); the CN seed matches 2.0/2.5/3.0 and not retired 1.5.**
- **feat(sse):** Claude OAuth connections can opt in (per account, Edit connection → Claude section) to Claude Code's lower-priority lane and once-a-week session-limit reset. After the first 5-hour usage-wall 429 carrying `anthropic-ratelimit-unified-slow-offer: treatment`, OmniRoute retries the same account with `anthropic-usage-limit: slow` and keeps sending it until the window resets — the account keeps serving past the limit instead of being cooled down (slot_busy/529 wait the server's `slow-retry-after`, bounded by `slow-max-wait`). With auto-reset on, the wall first tries `POST /api/organizations/{org}/reset_rate_limits` (`juniper_tide`) and retries at full speed when the server grants it. Both default off; nothing is sent before the limit is hit.
- **feat(usage):** redeem **GLM Coding Plan Reset Cards** (`glm` / `glm-cn` / `glmt` / `zai`) from the Provider Limits UI — clear an exhausted 5-hour or weekly coding-plan window before it rolls over, via the new `/api/usage/glm-reset-card` route (`GET` lists, `POST` redeems). List and redeem requests egress through the connection's proxy and honor exclusive-lease isolation; z.ai's `requestId` is reused for retries of an ambiguous (transport-failed) redemption so a lost response cannot double-consume a card (in-memory, best-effort — restart the server and a fresh key is required). Responses are validated fail-closed (HTTP 200 alone is never treated as success), unavailable/expired cards are filtered and the list is sorted by earliest expiry, and the post-redemption quota refresh is best-effort: a refresh failure still reports the successful reset.
- **feat(routing): self-hosted unified OpenAI-compatible entry (`/v1/chat/completions`).** When `OMNIROUTE_SELF_HOSTED_PROVIDERS` (inline YAML) or `OMNIROUTE_SELF_HOSTED_PROVIDERS_FILE` is set, the existing `/v1/chat/completions` route diverts through the self-hosted provider adapters (`open-sse/services/providerAdapters.ts`) — OpenAI / Anthropic / local-compatible — instead of the cloud pipeline. Provider is auto-routed via the `x-omniroute-provider` header, a `provider/model` (or `provider::model`) model prefix, or the first configured provider; upstream credentials stay runtime-only and are stripped from echoed responses. Optional `OMNIROUTE_SELF_HOSTED_API_KEY` guards the entry with `Authorization: Bearer` (reserved for the D5 quota-key system); unset = open loopback/trusted-network route. Upstream failures return the standard OpenAI error shape (including a normalized 502 for unreachable providers). One OpenAI SDK snippet can now traverse multiple self-hosted providers without changing the client. (#RIC-738 / RIC-697 D4)
- **feat(routing): deterministic routing strategies for the self-hosted entry (`strategy:` block, M2/RIC-740).** The unified `/v1/chat/completions` entry (RIC-738) now accepts a declarative `strategy:` block — inline in the providers YAML or via `OMNIROUTE_SELF_HOSTED_STRATEGY` / `OMNIROUTE_SELF_HOSTED_STRATEGY_FILE` — expressing five explainable, non-predictive routing policies: blacklist / whitelist (hard filters), cooldown circuit breaker (`consecutiveFailures` + `cooldownMs`), cost-priority (cheapest `costPer1MInput` first), latency-aware (fastest recent average first), and an explicit `fallbackChain` order. The ordered candidate list is the fallback chain: a failed primary (network or non-2xx) falls through to the next candidate, and each failure feeds the breaker. Every response carries `x-omniroute-route-decision` — the one-line "why this model / why not that one" audit trail (D3). A pinned provider rejected by a hard filter returns `400` (never a silent re-route); no eligible providers returns `503` with the full explainable decision. No ML/predict dependency; malformed strategy config returns `500` rather than silently becoming a no-op. (#RIC-740 / RIC-697 D3)
- **feat(providers):** share the existing `api.x.ai/v1/models` discovery config with `xai-oauth` so SuperGrok OAuth connections pick up new Grok ids without a registry seed edit.
- **fix(video):** Honor `poll_interval_ms` and `max_polls` for Agnes and other video job providers so client-requested polling delays prevent upstream status-query rate limits.
- **fix(api):** `/v1/files` and `/v1/batches` now enforce one ownership rule everywhere — a dashboard session is the instance operator, an API key acts on its own records only, and a record with no owner is denied to every non-session caller. Previously a file or batch whose `api_key_id` was null (a dashboard-session or anonymous upload, or a batch artifact inheriting one) could be read, downloaded, deleted, cancelled or used as a batch input by any other key or by an unauthenticated caller (GHSA-2jm2-mpx8-6523), and `GET /v1/files` / `GET /v1/batches` returned every tenant's records to an anonymous or invalid-bearer caller under the default `REQUIRE_API_KEY=false` (GHSA-m3hp-hq9g-fpmv) — both lists now fail closed with a `401`, and only a dashboard session without a key reads the whole instance. The same shared rule lets the dashboard cancel any batch, not just unowned ones. Behaviour change: the anonymous upload → batch → download flow no longer works without an API key, since a null owner cannot be attributed. Subsumes [#13683](https://github.com/diegosouzapw/OmniRoute/pull/13683) — thanks @hartmark
- **fix(i18n):** every dashboard catalog other than `pt-BR` (64 locales) went through the same quality review `pt-BR` received in #13885 — each leaf changed by the 2026-09 retranslation was checked against its English source by the translation backend and rewritten where the meaning, placeholders, register or product terminology were off: 73,586 corrections net (75,263 applied, 1,677 that had turned a real translation into the plain English term reverted so the real-translation ratio gate stays where it was). `scripts/i18n/review-locale.mjs` now survives an upstream hiccup (per-batch retries with backoff, skipped batches listed in `_artifacts/i18n-review/<code>.skipped.json`), checkpoints the catalog every 25 batches instead of writing only at the end, and writes leaves whose own key contains a dot (`compliance.eventTypes["apiKey.ban"]`) instead of crashing.
- **fix(auth):** closed the JWT_SECRET bootstrap chain (GHSA-7pq4-8pvv-rx7r). The fresh-install bootstrap gate in `isAuthRequired()` now decides "loopback" from the trusted peer — the token-stamped real TCP peer the custom server writes, the pipeline's own locality verdict, or a real socket — and never from the client-controlled `Host` / `nextUrl.hostname` whenever a stamping server is in front (every supported runtime), so `Host: localhost` from a remote address no longer opens the window; the anonymous first-password write (`POST /api/settings/require-login`) is under the same loopback constraint instead of being open to every network peer, and `managementPolicy` hands its `peerContext` verdict down explicitly. `/api/settings/obsidian` (incl. `/webdav`, which mints reusable WebDAV Basic credentials for a caller-chosen root served before Next.js) joined `ALWAYS_PROTECTED_API_PATHS`, and `enableObsidianVaultSync()` refuses a vault that is, sits inside, or contains the data directory (realpath-resolved), so the WebDAV file service can no longer be pointed at `server.env` / `storage.sqlite`
- **fix(release):** the local merge-train now runs the four i18n contract gates on every combined tree before an `--admin` merge — `i18n:check-keys` (every `en.json` key in all 65 dashboard catalogs), `i18n:check-keys:cli`, `i18n:check-ratio` (real-translation ratio) and the docs drift gate (`check-translation-drift.mjs`) — so a PR that adds dashboard keys without translations or edits a documented page without its mirrors is ejected from the train instead of landing on the release tip. Three such landings reached `release/v3.8.51` in 48 h (#13670, `1b2349de`, `7f1b4a5e`) although each PR's own CI was red on the same gates, because the train validated only typecheck, size/complexity and changelog integrity.
- **Combo routing:** a context-cache-pinned model that returns `401` now falls through to the normal combo fallback loop instead of terminating the request, allowing other eligible connections or providers to serve it.
- **fix(docker):** bump the Bun image to 1.4.0, enable Turbopack on Bun, and port the node image's build memory guards so the `-bun` container builds fit the 16 GB GitHub runner instead of dying with `cannot allocate memory` ([#11719](https://github.com/diegosouzapw/OmniRoute/pull/11719)). Both images now default `OMNIROUTE_BUILD_WORKERS` to `2` (1 page-data worker) against the measured ~4.5 GB per-process RSS budget (#7518/#11663).
- **security(runtime):** fail closed on hostile thrown values and keep upstream text out of public error surfaces — the chat pipeline now reads rejection metadata through a safe accessor, sanitizes the message before it reaches call logs and console, and projects the failure-usage code onto the bounded public vocabulary; Perplexity's non-streaming quota/upstream error body sanitizes the upstream message and projects the provider-supplied error code; Arena (lmarena) maps every public failure onto a fixed vocabulary instead of echoing the upstream error; Notion's TLS transport failure sanitizes the transport error before it reaches the response body ([#11742](https://github.com/diegosouzapw/OmniRoute/pull/11742)).
- fix(resilience): only clear the combo-level LKGP pin when it names the target that actually failed, so an unrelated target skip under `auto`/`round-robin` no longer discards a valid pin for a healthy provider (#12235)
- **fix(sse):** 429 bodies phrased as `N API calls / month` (Cohere trial keys) now classify as `quota_exhausted` instead of a short transient `rate_limit`, so a spent monthly allowance is no longer retried every few seconds for the rest of the billing cycle ([#12252](https://github.com/diegosouzapw/OmniRoute/pull/12252)) — thanks @brick30llc-ctrl
- fix(cache): fold `response_format`/Responses-API `text.format` into the semantic cache signature so a `temp=0` request can no longer be served a stored response body with a different output schema (#12307)
- fix(gemini): preserve response-schema nullability across union flattening so a model with nothing to say returns a valid null instead of the string `"null"` or a fabricated value (#12308)
- **fix(combo):** a priority combo whose steps are different models on one Claude OAuth connection now falls through to the next step — a model-specific 404 or 5xx is scoped to the model instead of retiring the whole account, while a 429 stays account-wide ([#12334](https://github.com/diegosouzapw/OmniRoute/issues/12334))
- fix(api): restore the `name` field on non-streaming `/v1/responses``function_call` output items — a plain (non-namespace) tool call's identity restore was blindly applying the `_toolNameMap` alias-table fallback as a `{namespace, name}` object, silently blanking `name` to `undefined` (dropped entirely by JSON.stringify) and leaving Codex unable to dispatch the call, so it re-narrated its intent in a loop instead (#12370)
- **fix(memory):** extracted facts and oversized extraction input are now truncated at a word or sentence boundary instead of at a hard character offset. `sanitizeMatch()` (500-char fact cap) and `capExtractionText()` (64KB extraction-input cap) previously sliced at the exact limit, which could cut a fact mid-word or mid-clause; both now back the cut index off within an 80-char lookback window, preferring sentence-ending punctuation (`. ! ?`), then a plain word boundary, and only falling back to the original hard cut when neither is found — the same pattern already used for `compressToolResults` (#8169) — thanks @LeMonBLOCK ([#12383](https://github.com/diegosouzapw/OmniRoute/pull/12383))
- **fix(chatCore):** stop `executeWithUpstreamStartTimeout` leaking its abortPromise listener onto the long-lived client/stream signal, and stop `mergeAbortSignals` leaking per-attempt abort listeners, so a later hedge cancellation or client disconnect cannot reject an orphaned promise and take the process down (`Error [AbortError]: hedge-cancelled`). The crash guard also absorbs combo abort reasons (`hedge-cancelled`, `combo-per-model-timeout`) and raw string disconnect reasons as a last-resort net ([#12406](https://github.com/diegosouzapw/OmniRoute/pull/12406) — thanks @Beexly)
- **fix(db):** give `conversation_turn_nodes` its own independent retention knob (`retention.conversationTurnNodes`, default 30 days — matching `callLogs` so upgrading changes nothing until an operator overrides it) instead of sharing `callLogs`, and sweep orphaned `agentic_conversations` after the nodes expire (#12453).
- **fix(usage):** Render OpenRouter PAYG account credits as a metered quota when no per-key spending limit is set ([#12468](https://github.com/diegosouzapw/OmniRoute/pull/12468))
- **fix(cli):** `omniroute serve` no longer reports "Server did not respond within 60s" for a server that is actually up: the readiness probe's per-attempt timeout now escalates (2s, 4s, 8s, 15s, clamped to the time left in the budget) instead of aborting every attempt at a fixed 2s, so a health route that needs more than 2s for its first response is observed rather than repeatedly torn down. The timeout diagnostic now also states whether the port was accepting connections. ([#12484](https://github.com/diegosouzapw/OmniRoute/pull/12484))
- **fix(cli):** `omniroute serve` now checks whether the port is already owned before spawning anything, and reports the conflict with the owning PID plus the two ways out (`omniroute stop`, or `--port`). Previously it handed the conflict to the child process, which died with `EADDRINUSE` and was retried twice on the supervisor's restart budget, printing three identical raw Node stack traces without ever saying that another instance held the port. Because that happened after the pid files were written, the doomed second instance also de-registered the healthy running one, leaving `supervisor/.pid` pointing at the dead starter and `server/.pid` deleted. ([#12485](https://github.com/diegosouzapw/OmniRoute/pull/12485))
- **fix(sse):** Codex WebSocket transport (including the app-server) no longer fails to load in the Next.js standalone Docker runtime — the wreq-js loader now resolves its module name dynamically instead of a literal Turbopack could rewrite to an unreachable build-time symlink (#12491) — thanks @marshalfevzi
- **fix(devin):** treat Devin CLI model ids as literal — never strip or synthesize effort suffixes ([#12492](https://github.com/diegosouzapw/OmniRoute/pull/12492) — thanks @Neuron-Mr-White)
- **fix(command-code):** floor a tiny caller-set `max_tokens` (e.g. `64`) to `MUSE_SPARK_MIN_OUTPUT_TOKENS = 512` for muse-spark ids, detected through the prefix-aware `MUSE_SPARK_PATTERN` so provider-prefixed forms (`meta/muse-spark-1.2-contributor`, `cmd/meta/muse-…`) are covered in both `buildOpenAiBody` (the `/provider/v1/chat/completions` path from #12130) and `buildCommandCodeCliBody` (the `/alpha/generate` fallback) — the hidden server-side reasoning phase can no longer consume the whole output budget and answer HTTP 200 with null content (`out=64, reasoning=61`), mirroring the #11214 mitigation already shipped for opencode-go; a caller that sent no budget is left without one and budgets at or above the floor pass through untouched ([#12497](https://github.com/diegosouzapw/OmniRoute/pull/12497)) — thanks @Stazyu
- Fix `keys regenerate`/`keys reveal` in the CLI to fall back to the dashboard `/api/keys` route when an ID from `keys list` does not exist in the registered-keys store, closing an ID-namespace drift between the two API key families.
- **fix(cli):** Windows dashboard no longer reports Claude Code as `settings_found_binary_unresolved` when npm-global detection fails inside Electron. A failed `npm config get prefix` is no longer cached as permanent `""` (which deleted every npm-derived candidate for the process lifetime), Windows lookup PATH is enriched with npm-prefix / `%APPDATA%\npm` / nvm / `%ProgramFiles%\nodejs`, and stock Node MSI `.cmd` shims under Program Files remain an explicit safety net. Separate from the #7831`.ps1` / known-path fix for #7774. ([#12563](https://github.com/diegosouzapw/OmniRoute/issues/12563))
- **fix(pricing):** saving model pricing from the dashboard no longer fails with a 400 / `[object Object]` — sync-written pricing fields round-trip through PATCH and validation errors surface actionable details ([#12629](https://github.com/diegosouzapw/OmniRoute/pull/12629)) — thanks @wofiporia
- **fix(chat):** requests with null/non-object entries in `messages[]` are now rejected with a clear 400 instead of crashing translators with an HTTP 500 ([#12643](https://github.com/diegosouzapw/OmniRoute/issues/12643)) — thanks @soroush5
- **fix(sse):** Claude-native context handoffs now land in Anthropic's top-level `system` parameter instead of a leading `role: "system"` message, and the final Claude executor dispatch hoists any remaining leading prompt system/developer messages and relocates directive-only `output_config` envelopes away from `messages[0]`, preventing the `messages.0: use the top-level 'system' parameter` HTTP 400 on model switches ([#12668](https://github.com/diegosouzapw/OmniRoute/pull/12668)).
- Honor a model's declared `reasoning_efforts` vocabulary in the reasoning-routing rule gate: a model-scoped or connection-scoped rule forcing `max`/`ultra` is now treated as supported when the model's resolved capabilities list that tier (operator overrides apply to models without a static registry declaration), instead of being rejected by the hardcoded `gpt-5.6-*` regex. Custom OpenAI-compatible providers whose models accept `max` natively (for example Merge Gateway `zai/glm-5.3-flash`, which accepts `low|high|max`) can now use forced-max rules without the request failing with `Reasoning effort 'max' is not supported by the configured target`.
- **fix(open-sse):** `reasoning_details[].text` is now promoted to `reasoning_content` even when `reasoning` is also present, so OpenRouter thinking models (GLM-5.3-Flash, DeepSeek-V4-Flash, Kimi K3) no longer lose their thinking traces in clients that only read `reasoning_content` ([#12688](https://github.com/diegosouzapw/OmniRoute/pull/12688) — thanks @thomasmaerz)
- **fix(providers):** xAI requests no longer silently drop an assistant tool call sent in the legacy OpenAI `function_call` shape (instead of `tool_calls[]`) — the call is now translated into the xAI request the same way modern tool calls are (#12692) — thanks @soroush5
- **fix(providers):** xAI responses no longer report `total_tokens`/`totalTokenCount` as `0` when upstream usage uses the legacy `prompt_tokens`/`completion_tokens` names instead of `input_tokens`/`output_tokens` (#12700) — thanks @soroush5
- **fix(dashboard):** the Modal provider connection form now shows a Base URL field (placeholder `https://<workspace>--<app>.modal.run/v1`), so bring-your-own-deploy Modal connections can be validated and saved instead of failing outright — the server-side validator already required `providerSpecificData.baseUrl` ([#12704](https://github.com/diegosouzapw/OmniRoute/issues/12704))
- **fix(cursor):** Kimi-k3 / kimi-k3-high on the Cursor provider sometimes emit tool calls by imitating the executor's own history narration ("Assistant called tool … with arguments: …") instead of using structured tool calls, so clients received raw narration text plus native generation delimiters with `finish_reason: "stop"` — and the leaked turn compounded on every subsequent request via history re-send; the cursor executor now detects this shape and reassembles it into a structured `tool_calls` entry in both streaming and non-streaming finalization paths, gated on "no structured tool calls yet" so healthy turns are untouched ([#12723](https://github.com/diegosouzapw/OmniRoute/pull/12723))
- **fix(sse):** route the `dario` and `9router` request bodies through the internal-marker strip before they are serialized upstream — both executors override `transformRequest()` without calling the base implementation, so the internal context-relay / universal-handoff markers (`_omnirouteSkipContextRelay`, `_omnirouteInternalRequest`, `_omnirouteSkipUniversalHandoff`) reached strict OpenAI-compatible gateways and got the call rejected with HTTP 400 "Unsupported parameter(s)" ([#12729](https://github.com/diegosouzapw/OmniRoute/issues/12729))
- **fix(providers):** OpenAI-compatible model discovery now parses per-vendor-route `effort_values` (nested under `vendors.<vendor>.capabilities.reasoning` in `/v1/models`), intersected across vendor routes so a synced level is always honored on every route the model can land on; re-syncing a connection whose catalog declares this shape no longer silently resets the synced `supportedThinkingEfforts`/`defaultThinkingEffort` data ([#12730](https://github.com/diegosouzapw/OmniRoute/pull/12730))
- **fix(models):** a cold `GET /v1/models` on a large deployment no longer blocks the event loop for about a second at a time or overruns the 8s cold-build bound: since #12046 the built-in `auto/*` combos resolved catalog metadata for every target of every combo without memoizing or yielding, and they all draw on the same candidate pool, so 720 synced models took the build from ~4s to ~18s. Each distinct target is now resolved once per build, with a yield between misses ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))
- **fix(combo):** a `quota-share` combo whose steps carry no weight now rotates across its targets again instead of sending every request to the first one — the resolver turns an unset weight into 0 and #10881 made 0 mean "disabled", so an all-unweighted combo had no quanta and fell back to definition order; an explicit 0 still disables a target next to weighted siblings ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))
- **fix(codex):** the Codex WebSocket transport now emits a terminal `response.failed` (code `upstream_websocket_closed`) when the upstream socket closes before a terminal response event, instead of ending the client stream as if it had completed normally — preventing silent output truncation and allowing fallback/retry to trigger ([#12737](https://github.com/diegosouzapw/OmniRoute/pull/12737)).
- **fix(models):** preserve free-model metadata (`isFree`) discovered live from a provider through synced-model normalization, so free models no longer lose that flag before reaching the UI/consumers ([#12763](https://github.com/diegosouzapw/OmniRoute/pull/12763)) — thanks @keeltrace
-`/v1/models` combos whose merged `capabilities.vision` is `true` now also advertise `input_modalities: ["text","image"]` / `output_modalities: ["text"]` (synced modality intersections keep precedence), so models.dev-shaped clients no longer see a vision combo as text-only. (#12799 — thanks @aref-alapour)
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.