#13679 blocks the shipped INITIAL_PASSWORD placeholder at /api/auth/login when
the request is not loopback. /api/cli/connect verifies the same management
password, mints an admin-scoped oma_ access token on success, and is not in
LOCAL_ONLY_API_PREFIXES — so on a fresh install, where sync-env.mjs copies
INITIAL_PASSWORD=CHANGEME out of .env.example, the public default could be
exchanged for admin from anywhere the port is reachable.
Same gate, same audit shape (cli.connect.insecure_default_blocked). Loopback
pairing and any rotated password are untouched.
Refs #14486.
Hard Rule #16 had no automated enforcement: eight contributor commits reached release/v3.8.51 carrying `Co-Authored-By: Claude …`, `Codex <codex@openai.com>` and `Claude-Session:` trailers through squash-merge bodies (#14436).
The gate runs in three places: the husky commit-msg hook (local), the quality.yml fast-gates loop (PR→release/**) and a PR-only lint step in ci.yml (PR→main). It reads the event payload on PRs and no-ops elsewhere. Human co-authors are explicitly allowed — only AI/bot names, AI-owned e-mail domains and AI-generation footers are rejected.
Refs #14436.
The release tip went red on check:file-size after the 2026-09-22 merge wave:
#14179 grew open-sse/executors/opencode.ts past its frozen ceiling and the
PR->release fast-gates do not run check:file-size, so every merge-train
boarding afterwards inherits the red. Absorbed once at the tip under the
owner-approved train-rebaseline policy. Measured clean: check:file-size passes
on the tip with this entry.
* feat(db): per-request cost ledger + per-key tpm/rpm/monthly quota (RIC-741)
Add M3 cost transparency + team autonomy:
- request_cost_ledger: one row per completed call with provider/model/token/
unit-price/amount breakdown, written from the existing recordCost paths.
- api_key_quota_limits + api_key_quota_counters: KISS counter+threshold quota
for tpm (tokens/minute) and rpm (requests/minute) via 2-bucket sliding
window; monthly USD cap reads from the ledger month SUM.
- checkKeyQuota/recordKeyQuotaUsage domain gate (fail-open B16/B29), wired
into enforceApiKeyPolicy pre-request and the usage-record post hooks.
- /api/usage/key-quota route + setKeyQuotaSchema for per-key config.
- Migration 177; tests cover ledger traceability and tpm/rpm/monthly break.
* fix(db): renumber cost-ledger/key-quota migration to 180
177 and 178 already landed on release/v3.8.51 by the time this branch was
analyzed (177_provider_connection_synced_models_at.sql,
178_memory_fts_au_conditional.sql); 179 also landed since. Renumber to the
next free slot and fix the file's own internal comment to match.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(db): use the canonical toNumber helper instead of local redefinitions
ESLint no-restricted-syntax bars new local toNumber definitions in favor of
@/shared/utils/numeric (#7879, DRY extraction) — replace the 3 near-identical
local copies in costLedger.ts, keyQuota.ts and costLedgerRecorder.ts. Also
wires keyQuota.ts's getKeyQuotaStatus to reuse the already-defined
toIsoWindowStart helper instead of duplicating the window-start math inline,
which fixes the unused-var lint error on that function.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* refactor(sse): keep chatCore.ts under its frozen file-size ratchet
The recordCost() wiring for the per-request cost ledger added ~24 lines to
chatCore.ts, an already-frozen file (file-size-baseline.json caps it at
6146 lines). Extract the shared provider/model/tokens/serviceTier/requestId
breakdown into buildCostCtx() and the apiKeyInfo?.id && estimatedCost > 0
guard into recordChatCallCost() (both in src/domain/costRules.ts), and the
streaming ledger-details object into buildStreamLedgerDetails() in
streamingCost.ts. No behavior change: same 18 cost-ledger/domain-cost-rules
tests pass unmodified; net effect is chatCore.ts now at 6143 lines (3 under
the frozen ceiling) instead of 6165.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* docs: bump the migration count to 178 after the new cost-ledger/key-quota migration
check:docs-all (stale-migrations, STRICT) failed because README.md, AGENTS.md
and llm.txt (plus its 65 docs/i18n/*/llm.txt mirrors, which must be exact
body copies of the root file) still said "177 migrations" after this PR
added migration 181_request_cost_ledger_and_key_quota.sql, bringing the
real count to 178.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Ant Rich <ant@richants.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(providers): add Lyceum pay-per-use provider + credit quota
Lyceum (lyceum.technology) is an OpenAI-compatible, usage-based inference
provider. Registered as a first-class apikey provider mirroring the
llmgateway/openrouter pattern:
- registry/lyceum: buildOpenAiCompatibleRegistryEntry (base
https://api.lyceum.technology/openai/v1, live /models discovery,
passthrough). Generic DefaultExecutor handles chat/embeddings — no
custom executor or translator needed.
- apikey metadata card + AGGREGATOR_PROVIDER_IDS + PROVIDER_ENDPOINTS.
- lyceumQuotaFetcher: reads the credit balance from
GET /api/v2/external/billing/credits (pay-per-use), surfaced as a
"credits" window in Dashboard > Limits and quota-aware preflight.
Registered via quotaTrackersBatch (keeps chat.ts frozen).
- usage leaf + dispatch + fetcher/supported/apikey-limits lists + label.
- tests: registry-shape (wave1-c) + 16-case quota fetcher/usage suite.
- docs: changelog fragment, regenerated PROVIDER_REFERENCE, provider
count 354->355 across README/AGENTS/llm.txt (+42 i18n mirrors)/SVGs,
file-size baseline bump for the gateways.ts catalog entry.
* docs(providers): regenerate provider reference for Lyceum (359→360)
Rebased onto release/v3.8.51 after #12462 (LLM Gateway DevPass quota) merged
and the base advanced. The live registry now totals 360 providers, so the
auto-generated reference and the diagram/marketing counts are regenerated
against the new base:
- npm run gen:provider-reference → docs/reference/PROVIDER_REFERENCE.md
- 359→360 in README.md, AGENTS.md, llm.txt (provider count), package.json
description and the diagram SVGs (readme-hero, promise-pillars,
comparison-table, cli-terminal, tier-flow-dark/light)
- node scripts/i18n/sync-llm-mirrors.mjs → 65 locale mirrors
Addresses the maintainer review asking to rerun gen-provider-reference.ts and
push the doc files missing from the original diff, which left Docs Gates red.
* test(providers): refresh count assertions + golden snapshot for the new Lyceum provider
DeepSeek's primary public API is /chat/completions, not /responses. The
previous default caused multi-turn tool-call requests to fail with:
400 The reasoning_text in the thinking mode must be passed back to the API
because the Responses API requires the caller to echo reasoning_text on every
turn, while the Chat Completions protocol does not.
Changes:
- format: "openai-responses" → "openai"
- baseUrl: .../responses → .../chat/completions
- Move openai-responses to alternateFormats ("Responses-compatible") so
operators who explicitly need that path can still select it per-connection
- Update unit test assertions to match the new default and alternate count
* feat(sse): forward the auto mode classifier beta to Anthropic-format upstreams
* docs(changelog): fragment for the auto mode classifier beta pass-through
* refactor(sse): move the client beta application into anthropicHeaders
The #4425 port guard waits for the listen port to free up before respawning a
crashed child. It probed only 127.0.0.1, which does not detect the address
`omniroute serve` actually binds by default: 0.0.0.0.
Node sets SO_REUSEADDR on every listener it creates. On macOS/BSD that lets a
specific-address bind coexist with an existing wildcard bind (Linux still
rejects the overlap in LISTEN state), so binding 127.0.0.1:PORT succeeds while
another process holds 0.0.0.0:PORT. The probe therefore reported "free", the
wait resolved immediately, the respawned child failed with EADDRINUSE, and the
crash/restart cascade #4425 set out to fix continued — indefinitely under a
`KeepAlive` supervisor such as launchd or systemd.
Probe the wildcard and loopback (plus any caller-supplied host) and treat the
port as busy if any of them is occupied.
Verified on macOS 27.0 / Node 22.22.3: with a server holding 0.0.0.0:PORT,
`isPortFree(PORT)` returned true before and now returns false, and a child
rebind of 0.0.0.0:PORT fails with EADDRINUSE as expected.
Tests: tests/unit/supervisor-policy-4425.test.ts
Co-authored-by: Gery.MK Song <gery@macunzip.dev>
When stacked prompt compression telemetry was added, per-engine breakdown
logs were written to `compression_engine_breakdown`. While the parent
`compression_analytics` table was regularly trimmed on an operator-configurable
retention policy (default 30 days) and purged on usage resets,
`compression_engine_breakdown` was omitted from both `cleanup.ts` and
`RESET_TARGETS`. In long-running deployments (5+ months), this table
accumulated 731,000+ unpruned rows without any deletion path.
Wired `compression_engine_breakdown` into `cleanupCompressionEngineBreakdown()`
with a 30-day cutoff, hooked it into nightly auto-cleanup, and added the
table to `RESET_TARGETS` so manual purges clean it completely.
Fixes#14268
Co-authored-by: Goni Sulaiman <gonisulaimann@users.noreply.github.com>
Co-authored-by: Koosha Paridehpour <KooshaPari@users.noreply.github.com>
* feat(kiro): expose the provider-native Opus 5 Max effort tier
Advertise "<base>-max" for Kiro's claude-opus-5 in the Claude effort catalog,
allow "max" in the Kiro effort values, and enable the adaptive-thinking
envelope for claude-opus-5.
The original change (the effortStandardization / adaptiveThinking edits and
the kiro-opus-5-max-effort test) is by tarciorick. The scoped
KIRO_OPUS_5_MAX_VARIANT_RE guard is an addition: the shared
CLAUDE_EFFORT_SUFFIX_RE must stay byte-identical to its sibling copies (drift
guard in claude-effort-variants.test.ts), so the synthesized -max id is
excluded by a separate regex scoped to that one id.
* chore(changelog): add fragment for #14284
* fix(antigravity): never persist or send the manual-project sentinel as a projectId
ensureAntigravityProjectAssigned() returns __REQUIRES_GCP_PROJECT__ when Google
does not auto-provision a project (BYOP). Only the in-request path guarded it;
token refresh, models discovery and the executor refresh persisted it as the
connection's projectId. The connection then looked configured, was PREFERRED by
preferAntigravityConnectionsWithStoredProject, and every request went upstream
as projects/__REQUIRES_GCP_PROJECT__ (HTTP 400, no lockout, no account
failover), so one poisoned account served nearly all traffic.
Add isUsableAntigravityProjectId() (rejects blank + the sentinel) and use it in
the persist helper, the stored-project selector, token refresh, models
discovery and the executor. A row already poisoned now behaves as "no project":
it takes the typed 422 GCP_PROJECT_REQUIRED path and is dropped by the selector
when a healthy sibling exists.
* chore(changelog): add fragment for #14282
When selecting the first-class `codex-app-server` provider directly through
the executor registry (`open-sse/executors/index.ts`), the lazy loader
instantiated `CodexAppServerExecutor({}, "codex-app-server")` without
supplying a WebSocket transport function, preventing any connection from
opening.
Additionally, `CodexAppServerExecutor.execute` passed the raw `input.model`
directly to `turn/start` without stripping reasoning suffix aliases (e.g.,
`gpt-5.5-medium`), which caused upstream app-server turns to fail because the
aliased name is not recognized as a valid model ID.
Fixes:
1. In `open-sse/executors/index.ts`, wire the shared WebSocket transport
(`codex.getCodexAppServerWebsocketTransport()`) into the `codex-app-server`
executor factory.
2. In `open-sse/executors/codex-app-server.ts`, use `splitCodexReasoningSuffix`
to send `baseModel` to `turn/start` while passing the selected effort
separately, prioritizing explicit model suffix selection over request body
defaults (#2331). Also support `body.reasoning_effort` in `extractEffort`.
Added regression tests in `tests/unit/codex-app-server.test.ts` verifying:
- baseModel extraction and effort derivation from model suffixes on `turn/start`
- precedence of suffix effort over body reasoning effort
- forwarding of body reasoning effort when the model is unsuffixed
- registry executor initialization with injected transport
Co-authored-by: Goni Sulaiman <gonisulaimann@users.noreply.github.com>
Co-authored-by: Aref Alapour <aref-alapour@users.noreply.github.com>
Gemini/antigravity returned a hard 400 ("Unknown name \"~optional\" ...
Cannot find field") the moment a tool's parameter schema contained a
`~optional` key, taking down every Gemini-family model in a combo's
fallback chain at once -- confirmed live: this exact error killed 100% of
a production combo's usable fallbacks for ~18 hours (the two remaining
candidates ahead of it were OpenCode free-tier models, permanently
inaccessible via API regardless).
Root cause: `~`-prefixed keys are the Standard Schema convention (Zod 4+,
Valibot, ArkType) for internal/vendor metadata, namespaced with a leading
`~` specifically so it can never collide with a real schema property name.
A tool built from one of those libraries leaked a literal `~optional` key
into a property's subschema. GEMINI_UNSUPPORTED_SCHEMA_KEYS already listed
the plain `"optional"` string, but `removeUnsupportedKeywords`'s exact-match
check doesn't catch the tilde-prefixed form, so it survived sanitization
and Gemini's OpenAPI 3.0 schema subset rejected the whole request.
Fix: strip any `~`-prefixed key at every schema level, the same way `x-`
vendor extensions are already stripped -- this covers `~optional` and any
other Standard Schema metadata key the same libraries may emit, rather than
only patching this one literal key.
Testing: new regression test confirmed failing on unpatched code and
passing after the fix; full existing Gemini/schema-stripping test suite
(495 tests) still green.
/v1/combos hardcoded multimodal: false for any combo containing a
combo-ref, while /v1/models resolved the same combo through
resolveNestedComboTargets and intersected the real leaves' vision flags.
The two catalogs disagreed about the same combo (#14232).
projectCombo now accepts the combo collection and expands resolvable
combo-refs with the same resolver the routing runtime dispatches
against. Dangling refs and self-cycles contribute no targets and keep
the conservative false, matching the doc contract. The vscode and raw
vscode import surfaces get the same wiring.
Co-authored-by: Goni Sulaiman <gonisulaimann@users.noreply.github.com>
Anthropic's Models API reports the window as max_input_tokens and
the output cap as max_tokens. Putting max_tokens in the window
candidate list made Claude Opus 5 advertise 128K instead of 1M.
Read max_input_tokens for the window. Keep max_tokens on the
output-limit list next to max_output_tokens.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
Ensure internal control markers (e.g. _omnirouteSkipContextRelay,
_omnirouteInternalRequest) injected during universal/context handoff
are stripped at normalizeAttemptBody before reaching any executor.
Prevents leaks on custom executors that serialize request bodies
independently.
Includes cross-layer regression test asserting that universal handoff
dispatches contain no _omniroute* keys.
* fix(sse): stop racing the client first-byte watchdog and leaking Anthropic finish reasons
Two independent faults made combo streaming unusable from a strict OpenAI client
(oh-my-pi/omp) that had previously worked.
1. The early-stream keepalive threshold equalled the tightest client first-byte
watchdog. Every combo request reached the resolver as a bare name with no
provider prefix, so it took the 2 000 ms default — and a combo handler
essentially never resolves inside 2 s, because it probes candidate legs first.
The default threshold therefore *was* the combo time-to-first-byte: the
synthetic keepalive byte landed at 2 003 ms while omp aborted at 2 011–2 016 ms
having received nothing, so the request died as a 499 with no diagnosable error.
Price the default tier at half the observed watchdog (1 000 ms) and tighten the
keepalive cadence from 2 500 ms to 1 500 ms so the inter-byte gap stays inside
the same budget. The slow tier keeps its deliberately long value: for
browser-session and anonymous-fallback providers, committing early only adds SSE
framing to a request the caller already expects to wait on.
2. normalizeOpenAICompatibleFinishReason passed cross-vendor synonyms through raw,
so an Anthropic-style `end_turn` returned by an OpenAI-format leg landed on the
OpenAI wire. omp reads an unrecognized finish_reason as a provider fault and
fails the whole turn with `Provider finish_reason: end_turn` — after the text had
already streamed, discarding a completed assistant message. Map the Claude
stop_reason vocabulary onto its exact OpenAI equivalent, keeping the deliberate
raw passthrough for genuinely unknown values and for the abort reasons whose
whole purpose is to not present as a clean `stop`.
Also make the combo 403 actionable: the bare "Combo X is not allowed for this API
key" reads like a routing bug, so callers retry the same doomed model or fall
through a whole compaction cascade instead of adding the combo to the key.
* changelog: add the #14247 fragment
An empty completion from antigravity's Gemini can be a real answer:
some prompts legitimately produce no text, and the upstream reports a
normal terminal finish reason (STOP -> "stop"). The fake-success guard
in isEmptyContentResponse flagged these regardless, so the
non-streaming leg rewrote them into synthetic 502s that fed model
lockout — a few hundred such "failures" a day kept most of the
reporter's 22-connection pool excluded and starved unrelated clients.
The guard exists for free-tier/scraping providers whose failure mode is
an empty 200 shell (#13461), so scope the exemption the same way the
repo scopes classifyFakeSuccessBody: a trusted-provider allowlist. On
antigravity, an empty completion with a normal stop reason (openai
"stop", claude "end_turn") now passes through as a valid 200; every
other provider keeps the existing guard, and antigravity responses with
no terminal stop reason are still flagged.
OpenCode Go now serves a GA qwen3.8-max, but the built-in deprecation
table (written when the model shipped only under the -preview id)
rewrote it to qwen3.8-max-preview before dispatch, and the upstream
rejects the preview id with a 401. The provider-aware exemption in
resolveModelAlias never fired because the opencode-go static registry
lacked the GA id.
- declare qwen3.8-max in the opencode-go registry (qwen family there is
text-only and routes through the Claude translator per #2292)
- give the GA model its own MODEL_SPECS row instead of aliasing it to
the preview spec
The rewrite stays for preview-only providers (qoder, bailian-coding-plan,
qwen-cloud-token-plan) and for callers with no provider in hand.
* fix(proxies): classify refusal cause and hang, add bounded opt-in recovery pass
* docs(env): document PROXY_HEALTH_RECOVERY_INTERVAL_MS
The recovery-pass interval this PR adds is read from process.env, so the
env/docs contract gate (check-env-doc-sync) requires it in .env.example and
docs/reference/ENVIRONMENT.md.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Max <maxmad64@gmail.com>
Co-authored-by: maxmad64bis <maxmad64bis@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(providers): route opencode-zen GPT-5.6 family to the Responses API
Upstream serves the GPT-5.6 trio only on /responses; /chat/completions
answers 503 "Endpoint is unavailable" (live-verified 2026-09-19 against
opencode.ai/zen/v1 with the same key on both endpoints, gpt-5.6-luna;
sol/terra declared from the same upstream endpoint docs). The zen registry
tagged muse-spark-1.2 with targetFormat:"openai-responses" but never the
GPT-5.6 entries, so OpencodeExecutor.buildUrl() posted them to the chat
endpoint. #12196 made the same declaration for gpt-5.6-luna on opencode-go.
Test: tests/unit/opencode-zen-gpt56-responses-format.test.ts, red without
the registry change, green with it, muse-spark control included.
* chore(changelog): add fragment for #14230
Search proxy-log rows always read null (no response received), even when the
provider answered with a 429, 403, or 500, so operators could not tell a real
refusal from a transport failure. Forward response.status at the four
emitEvent call sites; locally synthesized codes (envelope, transport) stay
null, matching the chat writer. Verified: new test 6/6 RED-then-GREEN,
neighbor search-432 7/7.
Co-authored-by: Max <maxmad64@gmail.com>
* fix(db): return undefined for empty bun:sqlite .get() results
bun:sqlite's Statement.get() returns null when no row matches, while
better-sqlite3, node:sqlite and sql.js return undefined. The call sites
are typed and written against undefined (`get(...) as Row | undefined`,
and isExclusiveConnectionActivelyLeased compares with `!== undefined`),
so when the server runs under Bun every connection looked exclusively
leased: dashboard connection tests reported LEASE_ACTIVE, usage refresh
was deferred with 409, and the model-sync scheduler found no connections
to sync.
Normalize the no-row result in the Bun adapter so all drivers share one
contract. Covered by a driver-independent unit test that runs in the
Node shards and a Bun-only test against the real bun:sqlite driver.
* docs: add changelog fragment for #14203
Moonshot connections whose baseUrl is api.kimi.com/coding were classified
as Open Platform and queried users/me/balance, which those Allegro keys
reject. Membership windows live on GET /coding/v1/usages as
usages.limit_7d.used_ratio (and limit_5h). Host classification now treats
an explicit coding URL as Coding Plan, the dispatcher and combo preflight
follow it, and used_ratio is mapped onto Code 7d/5h.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
The /v1beta Gemini ingress converts gemini to openai chat format before
re-entering handleChat, but the request URL keeps its /v1beta path. With
no path branch, detectFormat's max_tokens heuristic misread the
converted body as claude: non-streaming replies came back anthropic-
shaped and were dropped by the route's OpenAI-to-Gemini converter, and
streaming replies were 200 SSE responses with zero bytes, which made
the antigravity CLI and the Google GenAI SDK loop forever.
Treat a /v1beta path as openai chat unless the body still carries the
raw gemini contents envelope, so client-raw-request contexts that pass
unconverted gemini bodies keep the existing gemini detection.
Fixes#14165
Co-authored-by: Goni Sulaiman <gonisulaimann@users.noreply.github.com>
The WHATWG URL parser drops the port when it equals the scheme default,
so an http proxy URL like http://user:pass@host:80 reached the probe
with an empty port and defaultPortForScheme handed back 8080. The probe
then failed, the request was rejected with Proxy Fast-Fail, and the
healthy connection went into cooldown.
The https and socks5 defaults were already correct; only the http case
was wrong. Proxies with explicit non-default ports are unaffected.
Fixes#14157
Co-authored-by: Goni Sulaiman <gonisulaimann@users.noreply.github.com>
Combos page resolves provider prefixes through the client-safe alias map instead of the server-side model service.
Co-authored-by: Max <maxmad64@gmail.com>
- Register omp in cliRuntime.ts getKnownToolPaths for Windows (omp.cmd, omp.exe, %LOCALAPPDATA%\omp\omp.exe, ~/.omp/bin/omp.exe) and POSIX (~/.omp/bin/omp)
- Fix double .omp typo in cliTools.ts config path documentation
- Switch omp-settings route to openai-models-list discovery mode with injectV1: false
- Add case "omp" to extractEndpointFromConfig in all-statuses route
- Handle omp YAML configuration in checkToolConfigStatus before JSON parsing
- Add unit tests in cli-runtime-detection.test.ts and check-tool-config-status.test.ts
- Enhance cli-settings-omp.test.ts with cross-platform environment isolation
* fix(v1beta): keep inlineData and parts sent next to a functionResponse
convertGeminiToInternal() read only text, functionCall and
functionResponse parts. An image, PDF or audio sent as inlineData to
/v1beta/models/{m}:generateContent never reached the provider, and an
image-only turn became an empty user message. A content holding a
functionResponse returned its tool messages alone, dropping every other
part; gemini-cli sends a binary file a tool read exactly that way.
Map inlineData in non-model turns to image_url data URLs, and split a
content with functionResponse parts into the responses and the rest
before conversion, as the Gemini request translator already does.
* docs(changelog): add fragment for #14173
---------
Co-authored-by: datrixlab <325650023+datrixlab@users.noreply.github.com>
* fix(guardrails): cache the vision-bridge no-candidate outcome
* docs(changelog): number the fragment for #14161
* docs(env): document OMNIROUTE_VISION_BRIDGE_NEGATIVE_CACHE_MS
The negative-cache TTL this PR introduces is read from process.env, so the
env/docs contract gate (check-env-doc-sync) requires it in .env.example and
docs/reference/ENVIRONMENT.md.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: cryptiklemur <cryptiklemur@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
No-auth model lock now surfaces as a retryable cooldown (wait or 429 with Retry-After) instead of 401, and request-scoped refusals skip the token refresh.
Co-authored-by: Max <maxmad64@gmail.com>
Fixes#14135
Ensure custom provider nodes configured with a prefix alias generate
model options with `qualifiedModel: <alias>/<model>` rather than
falling back to the raw internal database node ID. This ensures
proper routing through the configured prefix and accurate catalog
and capability lookups in /v1/models.
Picking a :<channel> tag without the -web suffix only fails at request
time for web-session providers (gemini-web, claude-web, claude-turnstile)
with a node_modules-looking playwright error and no remedy in the guide.
- Release Channels: what -web adds (runner-web: Playwright + Chromium),
the deferred failure symptom, and the remedy per install method.
- Dockerfile Stages: add the runner-web row + build example.
- Available Profiles: add the web profile row (SELF_HOST_GUIDE links
here for its web-cookie troubleshooting item); drop a stale count.
Closes#14105
Co-authored-by: Mistertelecom <noc@ysoftware.com>
#14111 (follow-up to #8459): function_call_output / custom_tool_call_output
that carry input_image parts kept only a placeholder — the image never
reached Chat-backed vision models (Codex view_image).
The tool message stays text-only (text + placeholder, no raw base64), and
each image is now lifted into a following multimodal user message as an
Chat Completions image_url part, in output order, detail preserved.
Closes#14111
Co-authored-by: Mistertelecom <noc@ysoftware.com>
CITATION_RE strips any [n] token and cleanResponse() runs over the whole
answer before tool mode parses <tool> text into tool_calls, so subscript
indexing was removed from code: print(arr[0], arr[12]) came back as
print(arr, arr), in rendered code blocks and in write_file arguments
alike.
Citation cleanup now skips protected regions — fenced code blocks, inline
code spans and <tool> payloads — via CODE_SPAN_RE and stripCitations().
Prose citations are stripped exactly as before, including the leading
space folded into CITATION_RE by #14009.
Closes#14121
Co-authored-by: Zicocoder <Zicocoder@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
POST /api/v1/batches returned the raw error.message from its catch block,
so a DB-layer failure could ship file paths, SQL fragments or internal
identifiers to the client. Route it through sanitizeErrorMessage() from
open-sse/utils/error.ts, the same shared sanitizer every other /v1 route
already uses, keeping the existing 400 + invalid_request_error shape and a
non-empty message fallback.
Adds route-level coverage driving the real POST handler with a real API key
and seeded input file: the batch INSERT is made to throw a path-and-stack
laden SQLITE_CANTOPEN through the repo's own setDbInstance seam (no module
mocking), asserting the path/filename/stack never reach the body, plus a
control that a clean message survives sanitization intact.
Closes#14089
Closes#13998
1. .env.example: CREDENTIAL_HEALTH_CHECK_INTERVAL default is 60m (3600000), not 5m (300000)
2. README.md: clarify OMNIROUTE_SKIP_POSTINSTALL only skips the native SQLite warm-up
3. TROUBLESHOOTING.md: add Windows PATH guidance for "omniroute is not recognized"
(The OpenCode header-defaults half of #13998 was already fixed upstream by #14013.)
Co-authored-by: Mistertelecom <noc@ysoftware.com>
Pins that finish_reason is 'tool_calls' when a tool call was emitted (toolCallIndex > 0)
or one is still open (currentToolCallId), and 'stop' otherwise — including the sticky
currentToolCallId OR-branch that keeps a mid-stream tool call from reporting as stop.
Co-authored-by: pacocartones <pacocartones@users.noreply.github.com>
OMNIROUTE_DEFAULT_OPENCODE_MODELS in src/index.ts lists 8 models, but the README said
'Default: 4 curated models' and 'the default 4 may be hidden'. The list grew to 8
(cross-checked against the sibling opencode-plugin README and the package tests). Correct
both mentions.
Co-authored-by: pacocartones <pacocartones@users.noreply.github.com>
Pins the local-index remap that fixed the 2026-09-02 output_index-gap incident:
raw upstream tool_call indices map onto a contiguous 0-based first-seen sequence,
idempotently, honouring a seeded next and coercing numeric/string keys.
Co-authored-by: pacocartones <pacocartones@users.noreply.github.com>
Pins two currently-untested behaviors of the OpenAI->Claude tool-result adjacency
repair: a tool_result arriving after intervening user text is moved adjacent to its
tool_use turn, and an unmatched tool_result is preserved as unpaired text rather than
dropped.
Co-authored-by: pacocartones <pacocartones@users.noreply.github.com>
parseInt(process.env.STREAM_HISTORY_MAX) returned NaN for a non-numeric value, so the
'completedStreams.length > MAX' trim never ran and history grew without bound (a slow
leak in a long-lived SSE process). Extract resolveMaxCompletedHistory(), which falls back
to 50 on non-numeric/negative input and honours valid values including 0. Adds a unit test.
Co-authored-by: pacocartones <pacocartones@users.noreply.github.com>
VALID_TRANSITIONS let INITIALIZED go only to CONNECTING or CANCELLED, but a stream can
fail before it connects (credential selection/setup throws). fail() then hit an invalid
transition, left the tracker in INITIALIZED with error set and completedAt null, and
archiveStream persisted that inconsistent summary. Add FAILED to the INITIALIZED
transitions. Adds a regression test.
Co-authored-by: pacocartones <pacocartones@users.noreply.github.com>
openaiToOpenAIResponsesRequest trimmed the id on the function_call side (assistant
tool_calls -> function_call) but not on the function_call_output side (tool/function
role). clampCallId only length-clamps, so a whitespace-padded id that a client echoes
on both sides was keyed trimmed on one side and untrimmed on the other; the
orphaned-output filter then dropped the tool result and the model answered as if the
tool never ran. Trim both sides to preserve the pairing invariant clampCallId exists
to protect. Adds a regression test with padded matching ids.
Co-authored-by: pacocartones <pacocartones@users.noreply.github.com>
Closes#13997
`config show` does not exist, and the config subcommand actually manages external CLI integrations, not OmniRoute's own envs. Changed to `config list`.
`models list` parses `list` as a provider filter, causing unexpected behaviors. The correct command is just `models`.
Co-authored-by: Mistertelecom <noc@ysoftware.com>
#14022 already documented OMNIROUTE_STRIP_SYSTEM_PREAMBLE and
allowlisted COMBO_LOOP_SAFETY_TIMEOUT_MS. The remaining mismatch is
the ENVIRONMENT.md sentence that still backticks the constant as if
it were operator-facing.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
Keep the emulated client fingerprint in lockstep with the CLI pinned in the Dockerfile: shared client constant, .env.example overrides, provider translate-path golden, executor header assertions, the caller-version fallback assertions from #13708, and the live env/stealth docs.
Co-authored-by: backryun <backryun@daonlab.local>
* docs(i18n): refresh README and ENVIRONMENT mirrors for the base edits of 2026-09-21
* docs(i18n): re-adopt mirrors reformatted by the pre-commit hook
* docs(i18n): adopt the mirrors whose state save lost the race
The train validated typecheck, file-size/complexity and changelog
integrity only, so combined trees that add en.json keys without catalogs
or edit docs without mirrors reached release/v3.8.51 three times in 48 h
(#13670, 1b2349de, 7f1b4a5e) while each PR's own CI was red on those
gates. i18n:check-keys, i18n:check-keys:cli, i18n:check-ratio and the docs
drift gate now run with the static gates.
* fix(build): ship httpClientAbortGuard.mjs in the pack artifact; validate input_tokens with Zod
Wave five of the release/v3.8.51 base-reds, part 1 — the two that matter.
#14064 restored server-ws.mjs's import of ./httpClientAbortGuard.mjs and the
assembleStandalone copy, but not the two pack-artifact policy entries that
were lost with it. Without APP_STAGING_ALLOWED_EXACT_PATHS the prepublish
prune deletes the file; without PACK_ARTIFACT_REQUIRED_PATHS nothing notices.
Every boot of the published package would die with ERR_MODULE_NOT_FOUND — the
3.8.47 head-response-guard class. Both closure suites (9/9) now enforce it.
#13910's /v1/responses/input_tokens read request.json() behind a hand-rolled
typeof check. Hard Rule #7 wants the boundary on Zod; the t06 guard caught it.
Same passthrough envelope the catch-all Responses route uses, since the
counters below already walk the fields defensively. 9/9 on the route's suite.
Five no-unused-vars left behind by the wave (cliRuntime execFileSync, arena
test symbols and a type, compression rmSync, waitForServer req) are removed.
The 'openwa routes removed without deprecation' entry from the #14101 run was
an artifact of that PR trailing its base — the gate is clean on the tip.
Refs #13866
* fix(compression): let anchored file-pack rules see the transformed text; align wave-5 guards
Wave five of the release/v3.8.51 base-reds, part 2.
One production defect. #12825 (Hungarian Caveman pack) stopped gating file-pack
rules with the English keyword list and tested the rule's own regex instead —
against `lowerResult`, a lower-cased copy of the ORIGINAL text that the loop
never refreshed. An anchored pattern like leader_phrases' `^(?:i will|…)`
therefore ran its prefilter on "sure, i will…", failed the anchor, and was
skipped; the rule that strips "I will " from every English response was dead
since the merge. The prefilter now sees the text as the rules so far have left
it. New test fails on the tip and passes here; all Caveman suites, Hungarian
included, are 95/95. A frozen no-unused-vars suppression on caveman.ts no
longer had a target and is pruned.
Two more TS2677 predicates of the kind #14101fixed: #13910
(rerankProviderNodes.ts, `n is RerankProviderNodeRow` on a Record row) and
#13957's mitm catalog (antigravity.ts, `c is DynamicCatalogModel` on a
literal-or-null). Both narrow by NonNullable of the element's own type; the
api-route typecheck was 285 against a baseline of 283 on the pristine tip.
The rest are guards trailing legitimate changes:
- #12663 made gemini-3.8-flash the catalog head; T28 pinned 3.7.
- #13863 put mimo-v2.5 into the shared vision heuristic on purpose (the base
model is multimodal, only the Pro variants are text-only). The safety test
now asserts the real invariant: base and :free aliases yes, -pro no.
- #12565 moved npm-prefix detection into cliRuntimeNpmPrefix.ts with a
process-lifetime cache that importFresh() does not reset; the case resets it.
#12565 also builds Windows candidates with path.win32 on purpose; the qodercli
test compared against POSIX path.join.
- #13990 (the 2 GB Docker image) copies better-sqlite3 with --chown; the guard
matched the flag order literally. Now flag-order tolerant, still fails when
--from=builder is removed.
- #13378 reintroduced public/openference.svg under a name #11750 retired for
missing provenance and swapped the Cerebras showcase cell for it. The cell is
back and the asset is gone; whether the new drawing counts as provenance is
the owner's call.
Refs #13866
* fix(i18n): translate the 7 sidebar-pin and Claude low-priority keys into all 65 locales
#7f1b4a5e (sidebar pinned items) and #1b2349de (Claude OAuth lower-priority /
auto-reset) landed with their 7 new keys in en.json only, which the vi and
pt-BR parity suites flag. Translated with the repo's own sync-ui-keys
--translate-markers against the .113 i18n instance (codex/gpt-5.6-sol-low):
+446 lines across 65 catalogs, zero __MISSING__ markers, placeholders intact.
vi.json also has two keys reordered to mirror en.json; values unchanged.
Refs #13866
* chore(quality): list the 8 covering tests the sixth wave added in stryker tap.testFiles
30 commits landed on release/v3.8.51 while wave five drained; eight new unit
tests cover mutated modules and were not in tap.testFiles, so their mutant
kills did not count and check:mutation-test-coverage --strict failed on the
merged tree. Appended at the end of the list, nothing reordered.
Refs #13866
* docs: document the five env vars of the 09-18 wave; regenerate the version-manager skill for the open-wa routes
check:docs-all: BRIDGE_PORT, ROUTER_URL and CERT_DIR (bin/antigravity-bridge.mjs,
#c74cea3d), OPENWA_SERVICE_PORT (src/lib/services/bootstrap.ts, #1e8c913c) and
NEXT_PUBLIC_PORT (src/shared/hooks/useDisplayBaseUrl.ts, #d715190b) were read
in code but absent from .env.example and docs/reference/ENVIRONMENT.md. Added
next to their neighbours, with the defaults the code actually uses (open-wa is
8323, not the 201xx range the other services sit in).
check:agent-skills-sync: the open-wa feature added eight /api/services/openwa/*
routes to docs/openapi.yaml without regenerating skills/omni-version-manager/
SKILL.md. Regenerated with the repo generator; the diff is exactly those eight
route sections.
Refs #13866
* test: register the crash guard in the pack snapshot; inventory #13874's refresh-lane row read
pack-artifact-policy pins the list of root runtime files check:pack-artifact
must find in the tarball; dist/httpClientAbortGuard.mjs joined
PACK_ARTIFACT_REQUIRED_PATHS in this PR and the snapshot follows.
#13874 re-reads the connection row inside the Claude refresh lane so a queued
health check does not POST a refresh token a Layer 2 refresh already rotated —
a state read, inventoried like the family-cooldown lookup (tokenHealthCheck.ts
2 -> 3).
Refs #13866
* chore(quality): list native-codex-auto-resume test in stryker tap.testFiles (#13180 landed without it)
* fix(release): drain the seventh base-red wave of release/v3.8.51 (9 tests + pack-policy + dashboard-typecheck)
Three production defects the tests caught:
- rateLimitManager: maxWaitMs=0 (the #12902 disable sentinel) hit #12715's
queue-budget gate as "0 ms left" and 503'd every protected request.
- emergencyFallback: #14006 silently switched the budget-exhaustion target
provider nvidia -> groq against ENVIRONMENT.md and the NIM snapshot; restored.
- claudeConnectionFields.ts vs ClaudeConnectionFields.tsx (#13074) differed only
by casing; helpers renamed to claudeConnectionFieldValues.ts.
Guards realigned to legitimate changes: #13874 rotation map (distinct token in
the error test), #13350 origin-IP denylist, #13318 shared-catalog growth
(counts by invariant), comboTargetKeyPolicy import in the telegram stub, the
22 README mirrors that #13940/#14106 stamped with the retired openference.svg
(translated Cerebras cells recovered from history, hashes re-stamped),
bin/antigravity-bridge.mjs allowed in the pack policy, and the two dashboard
typecheck regressions (typed pinned section, ComponentProps cast).
Refs #13866.
* test: type the #13848 Gemini pairing tests (no-explicit-any) and inventory the semantic-cache embedding picker's connection read
Both arrived with the tip merge: #13848 added 13 explicit any casts to
translator-openai-to-gemini.test.ts (no-explicit-any is an error under
tests/), and 7a921299's embeddingOptions.ts reads provider connections
once without a hard-session-lease inventory entry. Stale suppression
count pruned for the test file only.
Refs #13866.
* test: split the #13848 turn-pairing cases out of translator-openai-to-gemini.test.ts
The file sits exactly at its frozen size cap; typing the pairing tests
(no-explicit-any) pushed it 14 lines over. The two cases are a coherent
regression suite of their own, so they move to
translator-openai-to-gemini-turn-pairing-13848.test.ts (registered in
stryker tap.testFiles) instead of widening the baseline.
* docs(env): document BRIDGE_PORT, ROUTER_URL, CERT_DIR, OPENWA_SERVICE_PORT and NEXT_PUBLIC_PORT (Refs #13866)
check:env-doc-sync has been red on the release tip since these five vars
reached code without their .env.example / ENVIRONMENT.md entries:
bin/antigravity-bridge.mjs (BRIDGE_PORT, ROUTER_URL, CERT_DIR — #14006),
src/lib/services/bootstrap.ts + api/services/openwa/_lib.ts (OPENWA_SERVICE_PORT)
and src/shared/hooks/useDisplayBaseUrl.ts (NEXT_PUBLIC_PORT — #13533).
Defaults and source files copied from the reads themselves.
* chore(skills): regenerate omni-version-manager for the open-wa service routes (Refs #13866)
check:agent-skills-sync (Merge integrity job) has been red on the tip since
the open-wa embedded-service routes reached docs/openapi.yaml without the
generated SKILL.md being refreshed. Output of
scripts/skills/generate-agent-skills.mjs --apply, no hand edits: the eight
/api/services/openwa/* operations.
* fix(types): make the two TS2677 type predicates sound (Refs #13866)
check:api-typecheck has been red on the tip with two "type predicate's
type must be assignable to its parameter's type" errors:
- src/app/api/v1/_shared/rerankProviderNodes.ts (#13733): the read cache
hands back `Record<string, unknown> | null`, and an interface whose members
are all optional is not assignable to an index-signature type. Narrow to the
non-null record and assert the row shape afterwards.
- src/mitm/handlers/antigravity.ts (#14006): the map callback returned
`{ displayName: string }` while DynamicCatalogModel declares it optional, so
the predicate could not be proven. Type the callback's return explicitly and
filter on `!== null`.
No runtime change; rerank-remote-provider-nodes / rerank-local-node-shapes /
mitm-handler-antigravity stay green.
* fix(lint): clear the 92 ESLint errors the lint gate reports on the tip (Refs #13866)
- tests/unit/translator-openai-to-gemini.test.ts: #13848 / #13318 added 13
`any` casts/params on top of the 74 frozen for the file, so ESLint reported
all 87. Typed them (GeminiRequestWithContents / GeminiToolPart, and the
existing GeminiRequestWithConfig) and pruned the file's suppression to the
new count of 71 — nothing else in eslint-suppressions.json changes.
- no-unused-vars: execFileSync import (src/shared/services/cliRuntime.ts,
#12565), getArenaEloSyncStatus + makeLeaderboardMap + ArenaLeaderboardMap
(tests/unit/arena-elo-sync-redesign.test.ts, #13446), rmSync
(compressionAnalyticsWriterFlatRate.test.ts, #13446), `req` → `_req`
(waitForServer-slow-first-response.test.mjs).
translator-openai-to-gemini 48/48; arena-elo-sync-redesign,
compressionAnalyticsWriterFlatRate, waitForServer-slow-first-response green.
* fix(compression): stop skipping anchored Caveman rules that only match after earlier rules
#12825 (Hungarian pack) replaced the English keyword prefilter with a
`rule.pattern.test(lowerText)` pre-check for every file-based rule, including
the default `en` pack. `lowerText` is the ORIGINAL message, so anchored rules
such as `leader_phrases` (`^i will …`) — which only match after `pleasantries`
strips "Sure, " — were dropped before they could run. `caveman-v379` caught the
regression ("I will ensure …" survived at full intensity).
Tag file-based rules with their pack language in ruleLoader and let the
keyword prefilter apply to `en`/built-in rules only; non-English packs (which
reuse English rule names) simply run their localized regex, which is what the
pre-test cost anyway. Drops the now-unused CAVEMAN_RULES import and prunes the
already-stale `caveman.ts` no-unused-vars suppression (0 violations on the tip)
that blocked the pre-commit hook for any change to this file.
Refs #13866
* test(models): align catalog and vision-heuristic guards with the tip's intended contracts
Three base-reds where the production change was deliberate and the pinned
guard was simply not bumped by the PR that changed the contract:
- agy-antigravity-shared-catalog-12724: #13318 added the three Gemini 3.8
Flash tiers (high/medium/low, no "-tiered" endpoint for 3.8) to the shared
Antigravity/AGY base, 10 -> 13. Pin the new size in one constant and make the
buildSurfaceCatalog delta assertions relative to it.
- t28-model-catalog-updates: #12663 (issue #12638) registered gemini-3.8-flash
at the head of the AI Studio fallback catalog as the current Flash default;
assert 3.8 first and keep 3.7 present.
- command-code-mimo-v2-5-safety: #13863 (issue #13847) added an explicit
"mimo-v2.5" fragment to the shared vision heuristic so provider-qualified and
`-free` aliases keep their vision flag. The guard's real concern (the
"mimo-vl" fragment must not cover "mimo-v2.5") is asserted on the fragment
itself; the bare id is now vision by heuristic on purpose, and the Pro
text-only sibling stays excluded.
Refs #13866
* test(cli): follow the #12565 cliRuntime module split in the npm-prefix and qodercli guards
#12565 (issue #12563) moved the npm global-prefix cache out of cliRuntime.ts
into cliRuntimeNpmPrefix.ts and built the Windows known-bin candidates with
`path.win32` (cliRuntimeWindowsNode.ts) so they stay Windows-shaped when
`process.platform` is mocked on a POSIX runner. Two pre-existing guards
depended on the old layout:
- cli-runtime-extended "resolves known binaries from npm global prefix":
importFresh() only re-evaluates cliRuntime.ts; the prefix cache now lives in
a module that stays shared across cases, so a real `npm config get prefix`
from an earlier case was cached and the mocked execFileSync never ran. Reset
the cache with the helper #12565 exported for exactly this in afterEach.
- qodercli-windows-resolve-6263: compare against `path.win32.join` — identical
to `path.join` on a real Windows host, which is the behaviour under test.
Production behaviour is unchanged on both platforms.
Refs #13866
* test(auto-update): write the source-mode log inside the test's own temp dir
The launchAutoUpdate case pointed AUTO_UPDATE_LOG_PATH at a fixed, world-shared
`/tmp/auto-update-source.log`. On the .113 runner the suite executes both as
`root` and as `runner` (uid 1001): the file survives owned by whoever ran
first (`-rw-r--r-- root root`), and the next `openSync(logPath, "a")` fails
with EACCES for the other user. Reproduced locally by making the shared file
read-only; production code is untouched (autoUpdate.ts last changed in #9354).
Use a per-test mkdtemp path for the source-mode log and clean the whole temp
root in the existing finally block.
Refs #13866
* fix(dashboard): rename claudeConnectionFields.ts so it no longer case-collides with ClaudeConnectionFields.tsx
#13074 added two modules to the provider-detail modals directory whose names
differ only by casing: `ClaudeConnectionFields.tsx` (the component) and
`claudeConnectionFields.ts` (the value/patch helpers). On a case-insensitive
filesystem the pair breaks the webpack build (#6584 guard), and esbuild's
resolver already picks the `.tsx` for the extension-less `./claudeConnectionFields`
specifier, so the provider-detail client entry failed to bundle ("No matching
export ... for import claudeConnectionFieldPatch").
Rename the helper module to `claudeConnectionFieldValues.ts` (the same naming
the sibling `quotaScrapingFieldValues.ts` uses) and point the only importer,
EditConnectionModal.tsx, at the new name. Greens
tests/unit/case-collision-6584.test.ts and
tests/unit/media-page-client-browser-bundle.test.ts.
Refs #13866
* fix(build): allowlist dist/httpClientAbortGuard.mjs so the published tarball keeps the server-ws crash guard
#14064 (re-land of #13636) made scripts/dev/standalone-server-ws.mjs import
./httpClientAbortGuard.mjs and taught assembleStandalone to copy the shared
implementation next to dist/server-ws.mjs — but never registered the file in
scripts/build/pack-artifact-policy.ts. The prepublish prune deletes anything
outside APP_STAGING_ALLOWED_EXACT_PATHS, and check:pack-artifact only fails on
PACK_ARTIFACT_REQUIRED_PATHS entries, so the next `omniroute` tarball would
boot straight into ERR_MODULE_NOT_FOUND (the #7065 / tls-options class the
closure tests exist to catch).
Add the bare and dist/ entries to both lists and extend the required-paths
snapshot in tests/unit/pack-artifact-policy.test.ts. Greens
tests/unit/pack-artifact-entrypoint-closures.test.ts and
tests/unit/pack-artifact-server-ws-closure.test.ts.
Refs #13866
* test(docker): accept --chown=node:node on the better-sqlite3 runner COPY
#14010 deliberately changed the runner-stage COPYs to `COPY --chown=node:node
--from=builder ...` (ownership at copy time instead of a second ~2 GB
`chown -R` overlay layer). The Dockerfile contract test still matched the old
`COPY --from=builder /app/node_modules/better-sqlite3` prefix and went red on
the tip even though the native-addon guard it protects is intact. Tolerate the
optional --chown flag; every other assertion (node-gyp rebuild, both
`test -f .../better_sqlite3.node` checks) is unchanged.
Refs #13866
* fix(api): validate /v1/responses/input_tokens bodies with Zod (t06)
#13167 added the local Responses token-count route with hand-rolled
`typeof` checks on `request.json()`. Hard Rule #7 and the t06 gate
(scripts/check/check-route-validation.mjs, mirrored by
tests/unit/route-body-validation-t06.test.ts) require every route that reads
request.json() to go through validateBody()/safeParse(), so the tip was red.
Add `v1ResponsesInputTokensSchema` (pins the wire types the counter reads —
model/instructions strings, input string-or-array, tools array — and lets
unknown keys through since they are counted, never forwarded) and run the body
through validateBody(); a type mismatch is now a 400 naming the field instead
of a silently ignored key. Regression test added to
tests/unit/responses-input-tokens-local-route.test.ts.
Refs #13866
* fix(docs): drop the retired openference.svg asset reintroduced by #13378
`openference.svg` is one of the 78 provider assets retired for missing
provenance (tests/unit/provider-assets-generic-fallback.test.mjs freezes that
list and forbids any tracked surface from referencing a retired name). #13378
added a new hand-drawn `public/openference.svg` outside the manifest-audited
public/providers/ tree and pointed the README free-tier table (plus the 22
i18n mirrors that carry the row) at it, which put the retired name back on a
tracked surface and left an unaudited asset in the package.
Use the generic fallback icon (`public/providers/cli-generic.svg`) the other
provenance-less providers already use, delete the unaudited file, and adopt
the mechanical README edit into .i18n-state.json
(`i18n:run -- --adopt --files=README.md`, no API calls) so the i18n drift gate
does not flag README.md as source-changed.
Refs #13866
* test(lease): classify the two connection-query sites added by #14159 and #13874
The hard-lease bypass inventory froze every getProviderConnections /
getProviderConnectionById site with a class; two landed on the tip without a
golden update:
- src/app/api/settings/cache-config/embeddingOptions.ts (#14159, re-land of
#12630): read-only listing that feeds the semantic-cache embedding dropdown,
same shape as the qdrant embedding-models route — class C.
- src/lib/tokenHealthCheck.ts 2 -> 3 (#13874): re-reads the row by id after an
unrecoverable refresh error to detect credentials rotated by a concurrent
Layer 2 refresh before deactivating — a state read, not dispatch; stays C.
Refs #13866
* fix(sse): restore nvidia as the emergency budget-fallback provider
#14006 (Antigravity MITM catalog injection) flipped
EMERGENCY_FALLBACK_CONFIG.provider from "nvidia" to "groq" in one line,
without touching ENVIRONMENT.md, .env.example, the chat.ts comment or the
NVIDIA hosted-model snapshot, all of which still promise
nvidia/openai/gpt-oss-120b. Operators without a Groq connection got the
original 402 back instead of the free reroute, and
chat-route-coverage ("uses the emergency fallback model on budget
exhaustion" / "returns the primary budget error when emergency fallback
also fails") went red on the tip.
Put the documented default back; the #14006 bridge tests exercise
bin/antigravity-bridge.mjs and do not read this config.
Refs #13866
* fix(resilience): keep maxWaitMs=0 a "no queue deadline" sentinel
#12902 released requestQueue.maxWaitMs=0 as the sentinel that disables
the queue-wait deadline, but the #12715 queue-budget gate in
withRateLimit() (`if (queueRemainingMs <= 0) throw`) read 0 as "budget
spent" and rejected every request on a protected connection with an
immediate 503 queue-budget error — the exact opposite of what the
setting promises. rate-limit-maxwaitms-disable-execution ("400ms job
completes without 504") was red on the tip.
When no caller budget is passed and the configured queue budget is 0,
skip the gate, never arm the queue-wait timer and hand
awaitProviderDefaultSlot no budget (it falls back to the window).
Execution stays bounded by executionMaxWaitMs and the upstream
fetch-start timeout, as before.
Refs #13866
* test: align three fixtures with the #13874, #13861 and #13350 contracts
Three base-reds that are deliberate contract changes, not defects:
- executor-default-base "refreshCredentials swallows refresh errors":
#13874 records rotations on the Layer 2 (no connectionId) refresh path
too, so the "refresh-me" token the previous case already rotated was
served from the rotation map without the network POST the test wanted
to fail. Use a token nobody rotated.
- telegram-keycache-bounded-13165: #13861 made comboTargetKeyPolicy
import isModelBlockedByPatterns from db/apiKeys; the loader-stubbed
module lacked it and the suite died at module load. Export an honest
"not blocked" stub (the test has no blocked models).
- upstream-headers-proxy-auth "ordinary headers are still allowed":
#13350 forbids the whole origin-IP forwarding set upstream (covered
by upstream-headers-sanitize). Swap x-forwarded-for for x-request-id.
Refs #13866
Third reconciliation pass of the living [3.8.51] section against release/v3.8.50..release/v3.8.51 (091589089c → 06f1df9d77): 364 fragments folded, 104 bullets generated for commits without a fragment, 254 fragment bullets linked and credited by origin commit, 1,393 bullets total, 226 external contributors (0 missing on cross-check). Hand-corrected credits: #12885 → @patrykkopycinski, #14159 → @BillyOutlast (feature bullet), #12972 → @IAMBOBJIM.
* docs(i18n): refresh the 689 mirrors left stale by the Codex quota outage
Section-level retranslation, for the 45 locales the interrupted run of
2026-09-18 left behind, of the docs the base edited that day (README,
SECURITY, API_REFERENCE, ENVIRONMENT, FEATURE_FLAGS, PROVIDER_REFERENCE,
REASONING_REPLAY, SOCKET_DEV_FINDINGS, EMBEDDED-SERVICES, admission-lanes,
...) on codex/gpt-5.6-sol-low once its quota returned: 65/65 locales, 0
failures, 2 h 40 with 6 workers. The drift gate's stale-target warning is
empty again.
* docs(i18n): adopt the tr mirrors whose state save lost the race
* docs(i18n): re-adopt the mirrors prettier reformatted in the pre-commit hook
* docs(i18n): refresh ENVIRONMENT and FEATURE_FLAGS mirrors for the base edits of 2026-09-21
* docs(i18n): re-adopt mirrors reformatted by the pre-commit hook
* docs(i18n): adopt the he mirrors whose state save lost the race
* docs(i18n): take the base README mirrors and state after the merge
The first autoSync cycle launched every connection at once. A host with
112 connections then held 112 catalog JSON parses on a cold heap and
died at the V8 cap. Cap in-flight fetches at 4 for the whole cycle, and
wait 90s so boot can serve traffic and the 30s cleanup has already run.
Signed-off-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
obsidianFetch passed the operator-controlled baseUrl straight to fetch(); POST /api/settings/obsidian then dialed it and persisted it for every later /api/obsidian/* call. The Local REST API legitimately lives on loopback / LAN / Tailscale, so the policy is the provider one (#5066): private hosts stay allowed, cloud-metadata and link-local are blocked unconditionally (route-level Zod refine + safeOutboundFetch block-metadata), redirects are never followed. Five TDD cases, red on the tip.
893fef9c updated feature-flags-settings.test.ts but not the sibling count guard in server-owned-tool-loop-flag.test.ts, so unit shard 2/4 failed on every PR cut from the tip. Refs #13866.
2026-09-21 18:18:58 -03:00
1635 changed files with 166920 additions and 106977 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: 4 curated models — see `OMNIROUTE_DEFAULT_OPENCODE_MODELS`. |
| `models` | `string[]` | No | Override the surfaced model catalog. Default: 8 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 4 may be hidden by your provider visibility settings.
- **OpenCode complains the provider has no models** — supply an explicit `models` list; the default 8 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-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._
_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._
### 📊 Release by the numbers
| | |
| --- | ---: |
| 👥 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 |
| 👥 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 |
**Entries by type**
| Type | Count |
| --- | ---: |
| 🐛 Fixes | 592 |
| ✨ Features | 131 |
| 🧹 Chore | 93 |
| 📚 Docs | 42 |
| 🧪 Tests | 32 |
| 🐛 Fixes | 962 |
| ✨ Features | 185 |
| 🧹 Chore | 112 |
| 🧪 Tests | 50 |
| 📚 Docs | 49 |
| ♻️ Refactor | 8 |
| 🏗️ Build | 7 |
| ⚡ Performance | 7 |
| ⚙️ CI | 2 |
| ⚙️ CI | 5 |
| 🔒 Security | 3 |
| 📦 Dependencies | 3 |
| ⏪ Reverts | 2 |
| 📦 Dependencies | 2 |
| 🔒 Security | 1 |
### 🏆 Top 25 contributors this cycle
_By commits in `091589089c..c0f92ec98a`, author identities consolidated via `.mailmap` and the merged PR's GitHub login. Bots excluded._
_By commits in `091589089c..06f1df9d77`, 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
@@ -895,6 +948,372 @@ _By commits in `091589089c..c0f92ec98a`, 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
@@ -1083,6 +1502,62 @@ _By commits in `091589089c..c0f92ec98a`, 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
@@ -1091,120 +1566,235 @@ 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 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."/>
<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."/>
<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: 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."/>
<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."/>
<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 list# ← runs against the REMOTE server
omniroute models # ← 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,6 +1006,10 @@ 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.