diff --git a/.env.example b/.env.example
index c809f7653c..20c43643a2 100644
--- a/.env.example
+++ b/.env.example
@@ -229,6 +229,15 @@ PORT=20128
# unaffected by this dev-only flag).
OMNIROUTE_USE_TURBOPACK=1
+# Disable systemd sd_notify (Type=notify / WatchdogSec=) even when running
+# under a systemd unit with NOTIFY_SOCKET set.
+# Used by: scripts/dev/systemd-notify.mjs. Set to 1 to disable.
+# OMNIROUTE_DISABLE_SD_NOTIFY=1
+
+# Injected by systemd when running under a service unit (sd_notify protocol).
+# Read by scripts/dev/systemd-notify.mjs — never set this yourself.
+# NOTIFY_SOCKET=/run/systemd/notify
+
# Skip the SQLite integrity health check on startup (faster boot on large DBs).
# Used by: src/lib/db/core.ts, src/lib/db/healthCheck.ts. Set to 1 to skip.
# OMNIROUTE_SKIP_DB_HEALTHCHECK=1
@@ -408,6 +417,15 @@ ALLOW_API_KEY_REVEAL=false
# by OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT and the heap-pressure shed instead. Set a positive
# value only on memory-constrained deployments that need a hard ceiling.
# OMNIROUTE_CHAT_HARD_MAX_MESSAGES=0
+
+# Skip OmniRoute's local context-window and max-input-token check for direct
+# single-model requests. Default: false (dangerous opt-in).
+# The upstream provider still enforces its real limits, so enabling this can
+# replace an early OmniRoute 400 with an upstream context-length error.
+# Prompt compression and the model's own output-token cap remain active.
+# Also configurable from Dashboard > Settings > Feature Flags; no restart is
+# required. Used by: src/shared/utils/featureFlags.ts and open-sse/handlers/chatCore.ts.
+# DISABLE_CONTEXT_WINDOW_CHECKS=false
# How long a heavy request waits for heavyweight capacity before a retryable 503.
# A short bounded wait serializes agent bursts instead of an instant 503; 0 = instant.
# Default 2000 (2s).
@@ -696,6 +714,11 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# ALL_PROXY=socks5://127.0.0.1:7890
# NO_PROXY=localhost,127.0.0.1
+# Pin the echo-IP target used by proxy egress probes. Unset, the probe tries
+# api64.ipify.org then api4.ipify.org so IPv4-only tunnels are not reported dead.
+# Used by: src/lib/proxyEchoTarget.ts.
+# OMNIROUTE_PROXY_ECHO_URL=https://api4.ipify.org?format=json
+
# Max concurrent sockets per cached HTTP/SOCKS proxy dispatcher.
# Long-lived SSE streams such as Codex /v1/responses need more than one
# connection when multiple requests share the same account-level proxy.
@@ -876,13 +899,21 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# Set to 0/false/off to skip compression entirely. Default: rtk
# OMNIROUTE_MCP_DESCRIPTION_COMPRESSION=rtk
+# Abort budget (ms) for MCP-server internal management reads (health, resilience,
+# combos, quota, usage). Default: 10000. Used by: open-sse/mcp-server/fetchTimeout.ts
+# OMNIROUTE_MCP_FETCH_TIMEOUT_MS=10000
+
+# Abort budget (ms) for MCP hops that wait on a provider (route_request, web_search,
+# web_fetch). Default: 60000. Used by: open-sse/mcp-server/fetchTimeout.ts
+# OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS=60000
+
# Model catalog sync interval in hours.
# Used by: src/shared/services/modelSyncScheduler.ts — periodic model refresh.
# Default: 24
# MODEL_SYNC_INTERVAL_HOURS=24
# Provider limits sync interval in minutes (rate limit windows, quotas).
-# Used by: src/server-init.ts — polls provider health endpoints.
+# Used by: src/lib/usage/providerLimits.ts — polls provider health endpoints.
# Default: 70
PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
@@ -1033,6 +1064,10 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# Used by: src/lib/db/core.ts::getDbHealthCheckIntervalMs().
#OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS=21600000
+# WAL truncate cadence override (ms). Set to 0 to disable. Default: 21600000 (6h).
+# Used by: src/lib/db/core.ts::getWalTruncateIntervalMs().
+#OMNIROUTE_WAL_TRUNCATE_INTERVAL_MS=21600000
+
# Skip the Redis-backed auth cache used by API key lookups (forces DB reads).
# Used by: src/lib/db/apiKeys.ts. Set to 1 to disable. Default: enabled.
#OMNIROUTE_DISABLE_REDIS_AUTH_CACHE=0
@@ -1348,6 +1383,14 @@ CURSOR_USER_AGENT="Cursor/3.4"
# FETCH_BODY_TIMEOUT_MS=600000 # Time to receive full response body
# FETCH_CONNECT_TIMEOUT_MS=30000 # TCP connection establishment (default: 30s)
# FETCH_KEEPALIVE_TIMEOUT_MS=4000 # Keep-alive socket idle timeout (default: 4s)
+# OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS=30000 # Bounded response-start window per direct
+# # (no-proxy) attempt (#10214). A silently-dropped
+# # pooled keep-alive socket surfaces no transport
+# # error, so without this bound a direct request can
+# # stall until undici's headersTimeout (600s) or the
+# # caller's deadline; on expiry the request retries
+# # once on a fresh no-keep-alive socket. 0 disables
+# # the bound (default: 30000 = 30s).
# Default timeout (ms) for src/shared/utils/fetchTimeout.ts. Acts as the
# fallback when FETCH_TIMEOUT_MS is unset. Default: 120000 (2 min).
@@ -1402,6 +1445,14 @@ CURSOR_USER_AGENT="Cursor/3.4"
# OMNIROUTE_PPLX_TLS_TIMEOUT_MS=30000
# OMNIROUTE_PPLX_TLS_GRACE_MS=10000
+# ── Perplexity web: built-in-search hint ──
+# Used by: open-sse/executors/perplexity-web/protocol.ts — appends "You have
+# built-in web search. Answer questions directly using search results." to the
+# caller's system message. Off by default: Perplexity's answer engine searches
+# anyway, and for coding clients the sentence leaks into replies as
+# meta-commentary. Set to 1/true/yes/on to restore the old behavior.
+# OMNIROUTE_PPLX_SEARCH_HINT=0
+
# ── Grok web TLS sidecar (Chrome-fingerprinted client) ──
# Used by: open-sse/services/grokTlsClient.ts — wire-level timeout for the
# bogdanfinn/tls-client koffi binding and the JS-side grace window layered on
@@ -1975,6 +2026,16 @@ APP_LOG_TO_FILE=true
# Reachability probe target for the scheduler and the auto-test endpoint.
# Point it at an internal/self-hosted URL to avoid the public default.
# PROXY_HEALTH_TEST_URL=https://httpbin.org/ip
+# Probes started at once per batch, for the scheduler and the auto-test endpoint.
+# Floored at 1 and capped at 50. Default: 10.
+# PROXY_HEALTH_TEST_CONCURRENCY=10
+# Delay in ms between two probe departures inside a batch. Without it the whole batch
+# leaves at once and a shared egress IP can trip a rate-limited target. 0 disables the
+# spacing; capped at 5000. Default: 100.
+# PROXY_HEALTH_TEST_STAGGER_MS=100
+# Set "false" to stop probing the real host of a proxy's assigned provider (GET /models,
+# no API key) and always use the generic target above instead. Default: enabled.
+# PROXY_HEALTH_USE_PROVIDER_TARGET=true
# Set "true" to let the scheduler auto-remove proxies after repeated failures.
# PROXY_AUTO_REMOVE=false
# Consecutive failures before an auto-remove fires. Default: 3.
@@ -2117,6 +2178,19 @@ APP_LOG_TO_FILE=true
# Used by: open-sse/utils/cursorAgentCliVersion.ts. Default: detect local install, else pin.
# CURSOR_AGENT_CLI_VERSION=2026.07.08-0c04a8a
+# Path to the Cursor Agent binary used for image generation.
+# Used by: open-sse/handlers/imageGeneration/providers (CURSOR_IMAGE.md).
+# CURSOR_AGENT_BIN=/path/to/agent
+
+# Cursor image-generation wall clock (ms). Default: 210000.
+# CURSOR_IMG_TIMEOUT_MS=210000
+
+# Shared-seat concurrency gate for Cursor image jobs. Default: 2.
+# CURSOR_IMG_MAX_CONCURRENT=2
+
+# Override Cursor CLI --model for image jobs. Default: request model / auto.
+# CURSOR_IMG_MODEL=auto
+
# Cursor Agent CLI data directory override (versions live under
/versions/).
# Used by: open-sse/utils/cursorAgentCliVersion.ts. Default: ~/.local/share/cursor-agent (unix)
# or %LOCALAPPDATA%\cursor-agent (win32). Official agent CLI also honors this var.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 8db8504007..3dfad903a7 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -50,13 +50,13 @@ updates:
# bumps; majors here need their own PR and a deliberate migration review.
- dependency-name: "ioredis"
update-types: ["version-update:semver-major"]
- # @huggingface/transformers is HARD-PINNED at 3.5.2 (exact, no caret) — FROZEN.
- # It is load-bearing for the LLMLingua ONNX compression engine (open-sse/services/
- # compression/engines/llmlingua/ — worker.ts pins @huggingface/transformers@3.5.2)
- # and for local memory embeddings (src/lib/memory/embedding/transformersLocal.ts),
- # and was VPS-validated at 3.5.2 (#4014). 4.x breaks both, and even 3.x minors must
- # be re-validated on the VPS — so freeze ALL auto-bumps (no update-types = ignore
- # every version). Migrate it intentionally, not via dependabot (#4050).
+ # @huggingface/transformers is VPS-validated at ^4.2.0 (migrated intentionally in
+ # #9962). It is load-bearing for the LLMLingua ONNX compression engine (open-sse/
+ # services/compression/engines/llmlingua/ — @atjsh/llmlingua-2@2.0.5 peers on
+ # "@huggingface/transformers": "^3.5.2 || ^4.0.0") and for local memory embeddings
+ # (src/lib/memory/embedding/transformersLocal.ts). Further majors must be re-validated
+ # on the VPS — so keep auto-bumps frozen (no update-types = ignore every version).
+ # Migrate it intentionally, not via dependabot (#4050).
- dependency-name: "@huggingface/transformers"
- package-ecosystem: "github-actions"
diff --git a/.github/workflows/radar-export.yml b/.github/workflows/radar-export.yml
new file mode 100644
index 0000000000..043de88d3d
--- /dev/null
+++ b/.github/workflows/radar-export.yml
@@ -0,0 +1,64 @@
+# Publica o export estável do catálogo consumido pelo OmniRoute Radar numa URL
+# fixa (asset de release `radar-export-latest`), para o servidor privado do Radar
+# (1 GB RAM, nunca clona/builda o OmniRoute) baixá-lo via `RADAR_EXPORT_URL` em
+# vez de depender do snapshot gravado no deploy. Fonte: scripts/release/radar-export.mjs.
+#
+# A URL estável resultante (definir em RADAR_EXPORT_URL no .env do radar-server):
+# https://github.com/diegosouzapw/OmniRoute/releases/download/radar-export-latest/export-omniroute.json
+name: Radar Export
+
+on:
+ workflow_dispatch: # o operador pode publicar sob demanda (de qualquer ref)
+ push:
+ branches: [main] # produção: só o catálogo do main clobra o asset estável
+ paths:
+ - open-sse/config/freeModelCatalog.data.ts
+ - open-sse/config/freeModelCatalog.ts
+ - open-sse/config/providerRegistry.ts
+ - open-sse/config/providers/**
+ - scripts/release/radar-export.mjs
+ - .github/workflows/radar-export.yml
+ schedule:
+ - cron: "17 6 * * 1" # semanal (segunda 06:17 UTC): mantém geradoEm/proveniência frescos
+
+permissions:
+ contents: read
+
+concurrency:
+ group: radar-export-${{ github.ref }}
+ cancel-in-progress: true
+
+env:
+ CI_NODE_VERSION: "24"
+
+jobs:
+ publish-export:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write # gh release upload — clobra o asset estável do export
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ persist-credentials: false # publish usa GH_TOKEN via gh release, não a credencial do checkout
+ - uses: actions/setup-node@v7
+ with:
+ node-version: ${{ env.CI_NODE_VERSION }}
+ cache: npm
+ - run: npm ci
+ - name: Generate catalog export with provenance
+ run: node --import tsx/esm scripts/release/radar-export.mjs "$RUNNER_TEMP/export-omniroute.json"
+ - name: Publish to the stable release asset
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ set -euo pipefail
+ TAG="radar-export-latest"
+ # Cria o release estável na primeira vez; nas seguintes só re-anexa o asset.
+ if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
+ gh release create "$TAG" \
+ --repo "$GITHUB_REPOSITORY" \
+ --title "Radar catalog export (rolling)" \
+ --notes "Export estável do catálogo OmniRoute para o Radar. Atualizado automaticamente; NÃO é um release de versão do produto." \
+ --latest=false
+ fi
+ gh release upload "$TAG" "$RUNNER_TEMP/export-omniroute.json" --repo "$GITHUB_REPOSITORY" --clobber
diff --git a/@omniroute/opencode-plugin/src/index.ts b/@omniroute/opencode-plugin/src/index.ts
index bc18518cad..7c196aeccb 100644
--- a/@omniroute/opencode-plugin/src/index.ts
+++ b/@omniroute/opencode-plugin/src/index.ts
@@ -1294,10 +1294,15 @@ export function mapRawModelToModelV2(
// `(providerID, modelID)`. If the raw id is already provider-prefixed
// (e.g. `cc/claude-opus-4-7` from the `cc` Claude Code alias, or
// `nvidia/llama-3-70b` from a provider that ships prefixed ids), leave
- // it as-is — double-prefixing breaks OC's lookup. Otherwise prefix with
- // the resolved `providerId` so a bare key like `claude-opus-4` parses as
- // `(omniroute, claude-opus-4)` and the credentials resolve correctly.
- id: raw.id.includes("/") ? raw.id : `${ctx.providerId}/${raw.id}`,
+ // it as-is — double-prefixing breaks OC's lookup. Bare **combo** ids
+ // (`owned_by: "combo"`, e.g. `gpt-5.6-sol`) must also stay unprefixed:
+ // OpenCode looks up `-m /` as model id `` under
+ // the plugin provider (#10345). Other bare ids still prefix with
+ // `providerId` so credentials resolve as `(omniroute, model)`.
+ id:
+ raw.id.includes("/") || raw.owned_by === "combo"
+ ? raw.id
+ : `${ctx.providerId}/${raw.id}`,
/**
* Display name. Falls back to raw.id when no enrichment is available;
* the caller (`createOmniRouteProviderHook`) overlays
diff --git a/@omniroute/opencode-plugin/tests/bare-combo-ids-10345.test.ts b/@omniroute/opencode-plugin/tests/bare-combo-ids-10345.test.ts
new file mode 100644
index 0000000000..f7afda9ab6
--- /dev/null
+++ b/@omniroute/opencode-plugin/tests/bare-combo-ids-10345.test.ts
@@ -0,0 +1,34 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+
+import { mapRawModelToModelV2 } from "../src/index.ts";
+
+test("mapRawModelToModelV2: bare combo ids stay unprefixed (#10345)", () => {
+ const combo = mapRawModelToModelV2(
+ {
+ id: "gpt-5.6-sol",
+ owned_by: "combo",
+ context_length: 272000,
+ max_output_tokens: 8192,
+ },
+ { providerId: "omniroute", baseURL: "https://or.example.com/v1" }
+ );
+ assert.equal(combo.id, "gpt-5.6-sol");
+ assert.equal(combo.providerID, "omniroute");
+
+ const slashed = mapRawModelToModelV2(
+ {
+ id: "cx/gpt-5.6-sol",
+ owned_by: "combo",
+ context_length: 272000,
+ },
+ { providerId: "omniroute", baseURL: "https://or.example.com/v1" }
+ );
+ assert.equal(slashed.id, "cx/gpt-5.6-sol");
+
+ const ordinary = mapRawModelToModelV2(
+ { id: "claude-primary", context_length: 200000 },
+ { providerId: "omniroute", baseURL: "https://or.example.com/v1" }
+ );
+ assert.equal(ordinary.id, "omniroute/claude-primary");
+});
diff --git a/AGENTS.md b/AGENTS.md
index b1bcfebafd..d4a7e7eb80 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -46,7 +46,7 @@ Repository map and Reference Documentation sections below.
## Project at a Glance
-**OmniRoute** — unified AI proxy/router. One endpoint, 341 LLM providers, auto-fallback.
+**OmniRoute** — unified AI proxy/router. One endpoint, 346 LLM providers, auto-fallback.
| Layer | Location | Purpose |
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -56,7 +56,7 @@ Repository map and Reference Documentation sections below.
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
-| Database | `src/lib/db/` | SQLite domain modules (154 migrations) |
+| Database | `src/lib/db/` | SQLite domain modules (157 migrations) |
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
| MCP Server | `open-sse/mcp-server/` | 109 tools (44 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes |
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 94438a0922..9c01b39a7a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -169,6 +169,7 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
- **security(search)**: block SSRF via `/v1/search` `provider_options.baseUrl` for the Firecrawl search provider — the client-controlled override is now validated as a public URL before it is used to build the server-side fetch target, so a caller with a valid API key can no longer redirect search requests at loopback, RFC1918, or cloud-metadata hosts — thanks @zmf963
- **providers**: honor `PATCH /api/providers/[id]` so `omniroute providers rotate` stops 405ing (the OpenAPI spec and CLI already use PATCH) (PR #10366)
+- **cli**: route provider test commands through configured connection test endpoints (#10570)
- **executors**: fix internal timeout misclassified as client disconnect (499) for 7 niche executors — pass TimeoutError reason to controller.abort() (#8197 side-finding)
- test(combo): guard auto/best-free never leaks the combo name as a model (#7754)
- fix(vision-bridge): describe-model no longer returns unreachable "openai/gpt-4o-mini" when every vision-capable provider is unreachable on the instance — returns null instead and surfaces a clear error (#8430)
diff --git a/Dockerfile b/Dockerfile
index de9b5a1499..8eca2c3bd2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -173,7 +173,7 @@ COPY . ./
RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-next-cache,target=/app/.build/next/cache \
mkdir -p /app/data \
&& npm run build \
- && node --input-type=module -e "import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; const standaloneRoot = '/app/.build/next/standalone/node_modules/'; const require = createRequire('/app/.build/next/standalone/package.json'); for (const pkg of ['@atjsh/llmlingua-2', '@huggingface/transformers', '@tensorflow/tfjs', 'js-tiktoken']) { const resolved = require.resolve(pkg); if (!resolved.startsWith(standaloneRoot)) throw new Error(pkg + ' resolved outside standalone: ' + resolved); await import(pathToFileURL(resolved).href); } const onnxRuntime = require.resolve('onnxruntime-node'); if (!onnxRuntime.startsWith(standaloneRoot)) throw new Error('onnxruntime-node resolved outside standalone: ' + onnxRuntime); await import(pathToFileURL(onnxRuntime).href);"
+ && node --input-type=module -e "import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; const standaloneRoot = '/app/.build/next/standalone/node_modules/'; const require = createRequire('/app/.build/next/standalone/package.json'); for (const pkg of ['@atjsh/llmlingua-2', '@huggingface/transformers', 'js-tiktoken']) { const resolved = require.resolve(pkg); if (!resolved.startsWith(standaloneRoot)) throw new Error(pkg + ' resolved outside standalone: ' + resolved); await import(pathToFileURL(resolved).href); } const onnxRuntime = require.resolve('onnxruntime-node'); if (!onnxRuntime.startsWith(standaloneRoot)) throw new Error('onnxruntime-node resolved outside standalone: ' + onnxRuntime); await import(pathToFileURL(onnxRuntime).href);"
# ── Runner base ────────────────────────────────────────────────────────────
FROM base AS runner-base
diff --git a/README.md b/README.md
index d04278cab3..0d5ea9f25f 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
# 🚀 OmniRoute — The Free AI Gateway
-
+
@@ -63,7 +63,7 @@
| | v3.8.49 | **v3.8.50** | `v3.8.51+` |
| ------------------------- | :-----: | :---------: | :---------: |
-| 🌐 Providers | 290 | **341** | more queued |
+| 🌐 Providers | 290 | **342** | more queued |
| 🧠 Documented models | 1185 | **1202** | — |
| 🖼️ Modality Bridge | — | 🆕 vision | video |
| 📡 Radar free catalog | — | 🆕 opt-in | — |
@@ -101,7 +101,7 @@
@@ -461,7 +461,7 @@ All **19** strategies — mix & match per combo step:
-
+📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)
@@ -559,7 +559,7 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style) round out the media surface. → [API Reference](docs/reference/API_REFERENCE.md)
- **🎨 Image / video / audio generation** — one API for media: xAI Grok Imagine & Novita AI video, ComfyUI, Freepik, Adobe Firefly, Microsoft Designer, Segmind, EdgeTTS. → [API Reference](docs/reference/API_REFERENCE.md)
- **🌍 Deployment & ops** — reverse-proxy `basePath`, browser-language auto-detect, per-key device tracking, root-less MITM trust, zh-TW localization. → [Environment](docs/reference/ENVIRONMENT.md)
-- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **341-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
+- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **346-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **📡 Routing transparency** — every response carries an `X-OmniRoute-Decision` header naming the strategy/provider/latency that served it, a new `cache-optimized` combo strategy + Auto-Combo `cacheAffinity` factor route repeat requests back to the connection holding the cached prefix, and a read-only `/v1/auto-combo/{channel}/candidates` endpoint exposes an `auto/*` channel's live candidate pool. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
- **⚡ Local performance & infra** — one-click local Redis, Cloudflare Workers / Deno Deploy relay deployers, Bifrost & Mux as supervised embedded services. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
@@ -642,11 +642,11 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
-## 🌐 341 AI Providers — 90+ Free
+## 🌐 346 AI Providers — 90+ Free
-> The most complete catalog of any open-source router: **341 providers**, **90+ with a free tier**, **56 free forever**.
+> The most complete catalog of any open-source router: **346 providers**, **90+ with a free tier**, **57 free forever**.
@@ -988,6 +988,8 @@ docker run -d --name omniroute --restart unless-stopped --stop-timeout 40 \
-p 127.0.0.1:20128:20128 -v omniroute-data:/app/data diegosouzapw/omniroute:latest
```
+`:latest` follows the highest **published** stable SemVer. It does not track git `main`. Pin `:X.Y.Z` for GitOps. See [Docker Release Channels](docs/guides/DOCKER_GUIDE.md#release-channels).
+
> **Pre-release Docker channel:** `diegosouzapw/omniroute:next` and
> `diegosouzapw/omniroute:next-web` follow the current default `release/v*`
> branch. These mutable tags are intended only for testing unreleased fixes and
@@ -1172,7 +1174,7 @@ Métricas de validação: 1002 vídeos rastreados · 7,069,190 visualizações c
diff --git a/bin/cli/api.mjs b/bin/cli/api.mjs
index fff6cf0829..6534f91095 100644
--- a/bin/cli/api.mjs
+++ b/bin/cli/api.mjs
@@ -52,6 +52,19 @@ function resolveUrl(path, opts) {
return `${getBaseUrl(opts)}${path.startsWith("/") ? path : `/${path}`}`;
}
+/** The machine-derived token is valid only for the local loopback server. */
+export function isLoopbackUrl(value) {
+ try {
+ const hostname = new URL(value).hostname.replace(/^\[|\]$/g, "").toLowerCase();
+ if (hostname === "localhost" || hostname === "::1") return true;
+ if (/^127(?:\.[0-9]{1,3}){3}$/.test(hostname)) return true;
+ if (/^::ffff:(?:127\.|7f[0-9a-f]{2}:)/i.test(hostname)) return true;
+ return false;
+ } catch {
+ return false;
+ }
+}
+
export async function buildHeaders(opts) {
const headers = new Headers(opts.headers || {});
if (!headers.has("accept")) headers.set("accept", "application/json");
@@ -87,10 +100,17 @@ export async function buildHeaders(opts) {
if (auth && !headers.has("authorization")) {
headers.set("authorization", `Bearer ${auth}`);
}
- // Inject machine-id derived CLI token; env var override for testing.
- const cliToken = opts.cliToken ?? process.env.OMNIROUTE_CLI_TOKEN ?? (await getCliToken());
- if (cliToken && !headers.has(CLI_TOKEN_HEADER)) {
- headers.set(CLI_TOKEN_HEADER, cliToken);
+ // Inject the machine-derived credential only for an explicit local loopback
+ // destination. Remote contexts and absolute remote URLs use scoped access
+ // tokens and must never receive this machine-bound local credential.
+ const destinationUrl = opts.destinationUrl ?? getBaseUrl(opts);
+ if (!isLoopbackUrl(destinationUrl)) {
+ headers.delete(CLI_TOKEN_HEADER);
+ } else {
+ const cliToken = opts.cliToken ?? process.env.OMNIROUTE_CLI_TOKEN ?? (await getCliToken());
+ if (cliToken && !headers.has(CLI_TOKEN_HEADER)) {
+ headers.set(CLI_TOKEN_HEADER, cliToken);
+ }
}
if (opts.idempotencyKey && !headers.has("idempotency-key")) {
headers.set("idempotency-key", opts.idempotencyKey);
@@ -195,8 +215,12 @@ function fetchOnce(url, init, timeoutMs) {
export async function apiFetch(path, opts = {}) {
const method = String(opts.method || "GET").toUpperCase();
const url = resolveUrl(path, opts);
- const headers = await buildHeaders(opts);
+ const headers = await buildHeaders({ ...opts, destinationUrl: url });
const body = serializeBody(opts.body, headers);
+ // Undici preserves custom headers across cross-origin redirects. A local server
+ // redirect must never turn the loopback machine credential into an outbound
+ // secret, so fail redirects whenever this header is present.
+ const redirect = headers.has(CLI_TOKEN_HEADER) ? "error" : opts.redirect;
const timeout =
opts.timeout ?? (Number.parseInt(process.env.OMNIROUTE_HTTP_TIMEOUT_MS || "", 10) || 30000);
const maxAttempts = opts.retry === false ? 1 : (opts.retryMax ?? RETRY_DEFAULTS.maxAttempts);
@@ -205,7 +229,7 @@ export async function apiFetch(path, opts = {}) {
let lastErr;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
- const res = await fetchOnce(url, { method, headers, body }, timeout);
+ const res = await fetchOnce(url, { method, headers, body, redirect }, timeout);
if (res.ok) return enrichResponse(res, opts);
if (attempt < maxAttempts && shouldRetryStatus(res.status, method, opts)) {
const delay = computeBackoff(attempt, res.headers.get("retry-after"));
diff --git a/bin/cli/commands/doctor.mjs b/bin/cli/commands/doctor.mjs
index 9ac34bf636..817013dd19 100644
--- a/bin/cli/commands/doctor.mjs
+++ b/bin/cli/commands/doctor.mjs
@@ -4,7 +4,9 @@ import os from "node:os";
import path from "node:path";
import { createDecipheriv, scryptSync } from "node:crypto";
import { fileURLToPath, pathToFileURL } from "node:url";
+import { isLoopbackUrl } from "../api.mjs";
import { resolveDataDir, resolveStoragePath } from "../data-dir.mjs";
+import { getCliToken, CLI_TOKEN_HEADER } from "../utils/cliToken.mjs";
import { printHeading } from "../io.mjs";
import { t } from "../i18n.mjs";
import { readDatabaseHealth, readEncryptedCredentialSamples } from "../sqlite.mjs";
@@ -378,11 +380,11 @@ function checkMemory() {
});
}
-async function fetchWithTimeout(url) {
+async function fetchWithTimeout(url, options = {}) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
try {
- return await fetch(url, { signal: controller.signal });
+ return await fetch(url, { ...options, signal: controller.signal });
} finally {
clearTimeout(timeout);
}
@@ -471,6 +473,98 @@ async function checkServerLiveness(options = {}) {
);
}
+export async function checkMachineTokenAuth(options = {}) {
+ if (process.env.OMNIROUTE_DISABLE_CLI_TOKEN === "true") {
+ return warn("CLI machine token", "CLI machine-token authentication is disabled", {
+ derived: false,
+ accepted: false,
+ disabled: true,
+ tokenExposed: false,
+ });
+ }
+
+ let url;
+ try {
+ const parsed = new URL(resolveLivenessUrl(options));
+ if (
+ !["http:", "https:"].includes(parsed.protocol) ||
+ parsed.username ||
+ parsed.password ||
+ !isLoopbackUrl(parsed.toString())
+ ) {
+ return warn(
+ "CLI machine token",
+ "Machine-token probes are limited to HTTP(S) loopback endpoints",
+ { derived: false, accepted: false, tokenExposed: false }
+ );
+ }
+ parsed.pathname = "/api/cli/whoami";
+ parsed.search = "";
+ parsed.hash = "";
+ url = parsed.toString();
+ } catch {
+ return warn("CLI machine token", "Could not resolve the management endpoint", {
+ derived: false,
+ accepted: false,
+ tokenExposed: false,
+ });
+ }
+
+ const token = await getCliToken();
+ if (!token) {
+ return fail(
+ "CLI machine token",
+ "Could not derive a machine token; verify the node-machine-id runtime is installed",
+ { derived: false, accepted: false, tokenExposed: false }
+ );
+ }
+
+ try {
+ const response = await fetchWithTimeout(url, {
+ headers: { [CLI_TOKEN_HEADER]: token },
+ redirect: "error",
+ });
+ if (response.ok) {
+ return ok("CLI machine token", "Server accepted the local machine token", {
+ url,
+ status: response.status,
+ derived: true,
+ accepted: true,
+ tokenExposed: false,
+ });
+ }
+ if (response.status === 401 || response.status === 403) {
+ return warn(
+ "CLI machine token",
+ "Server rejected the local machine token; if the CLI and server are on different hosts or container boundaries, run `omniroute connect --key `",
+ {
+ url,
+ status: response.status,
+ derived: true,
+ accepted: false,
+ containerBoundaryLikely: true,
+ tokenExposed: false,
+ }
+ );
+ }
+ return warn("CLI machine token", `Machine-token probe returned HTTP ${response.status}`, {
+ url,
+ status: response.status,
+ derived: true,
+ accepted: false,
+ tokenExposed: false,
+ });
+ } catch {
+ return warn("CLI machine token", "Machine-token endpoint could not be reached", {
+ url,
+ status: 0,
+ derived: true,
+ accepted: false,
+ tokenExposed: false,
+ });
+ }
+}
+
export async function collectDoctorChecks(context = {}, options = {}) {
const rootDir =
context.rootDir || path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
@@ -488,6 +582,7 @@ export async function collectDoctorChecks(context = {}, options = {}) {
if (!options.skipLiveness) {
checks.push(await checkServerLiveness(options));
+ checks.push(await checkMachineTokenAuth(options));
}
// CLI tool health checks
diff --git a/bin/cli/commands/providers.mjs b/bin/cli/commands/providers.mjs
index 91d60cead8..eb872241bf 100644
--- a/bin/cli/commands/providers.mjs
+++ b/bin/cli/commands/providers.mjs
@@ -129,7 +129,34 @@ function buildTestInput(connection, apiKey) {
};
}
-async function runProviderTest(db, connection) {
+async function testProviderConnectionThroughServer(connection) {
+ try {
+ const res = await apiFetch(`/api/providers/${encodeURIComponent(connection.id)}/test`, {
+ method: "POST",
+ body: {},
+ retry: false,
+ timeout: 30000,
+ acceptNotOk: true,
+ });
+ const data = res.ok ? await res.json() : { valid: false, error: `HTTP ${res.status}` };
+ return {
+ connection: publicConnection(connection),
+ ...data,
+ valid: data.valid === true,
+ skipped: false,
+ };
+ } catch (error) {
+ return {
+ connection: publicConnection(connection),
+ valid: false,
+ skipped: false,
+ error: error instanceof Error ? error.message : String(error),
+ statusCode: null,
+ };
+ }
+}
+
+async function runProviderTest(db, connection, { serverUp = false } = {}) {
// Only API-key connections can be probed with a stored credential. OAuth /
// no-auth connections have nothing for testProviderApiKey() to send, and
// getProviderApiKey() throws for them by design — reporting that as a FAILED
@@ -151,6 +178,9 @@ async function runProviderTest(db, connection) {
// means the CLI has no probe recipe, not that the provider is unhealthy.
// Persisting it would overwrite a good test_status with a failure.
if (result.unsupported) {
+ if (serverUp) {
+ return testProviderConnectionThroughServer(connection);
+ }
return {
connection: publicConnection(connection),
...result,
@@ -266,6 +296,7 @@ export async function runTestCommand(selector, opts = {}) {
}
export async function runTestAllCommand(opts = {}) {
+ const serverUp = await isServerUp();
const { db } = await openOmniRouteDb();
try {
const connections = listProviderConnections(db);
@@ -280,7 +311,7 @@ export async function runTestAllCommand(opts = {}) {
});
continue;
}
- results.push(await runProviderTest(db, connection));
+ results.push(await runProviderTest(db, connection, { serverUp }));
}
if (opts.json) {
diff --git a/bin/cli/commands/test-provider.mjs b/bin/cli/commands/test-provider.mjs
index 8802f75cd1..ec10c24649 100644
--- a/bin/cli/commands/test-provider.mjs
+++ b/bin/cli/commands/test-provider.mjs
@@ -38,12 +38,19 @@ export async function runTestProviderCommand(provider, model, opts = {}) {
}
const targetProvider = provider || "anthropic";
- const targetModel = model || "claude-haiku-4-5-20251001";
+ const connections = await _loadConnections();
+ if (!connections) return 1;
+ const connection = _resolveConnection(connections, targetProvider, model);
+ if (!connection) {
+ console.error(`Provider connection not found: ${targetProvider}`);
+ return 1;
+ }
+ const targetModel = model || connection.defaultModel;
const repeat = opts.repeat && opts.repeat > 0 ? opts.repeat : 1;
const results = [];
for (let i = 0; i < repeat; i++) {
- const result = await _runSingleTest(targetProvider, targetModel);
+ const result = await _runSingleTest(connection, targetModel);
results.push(result);
}
@@ -70,18 +77,10 @@ export async function runTestProviderCommand(provider, model, opts = {}) {
}
async function _runAllProviders(opts) {
- const res = await apiFetch("/api/providers?limit=200", {
- retry: false,
- timeout: 5000,
- acceptNotOk: true,
- });
- if (!res.ok) {
- console.error(t("test.noServer"));
- return 1;
- }
- const data = await res.json();
- const connections = (data.connections ?? data.providers ?? data.items ?? data).filter(
- (c) => c.authType === "apikey" || c.testStatus !== "unavailable"
+ const loaded = await _loadConnections();
+ if (!loaded) return 1;
+ const connections = loaded.filter(
+ (c) => c.isActive !== false && (c.authType === "apikey" || c.testStatus !== "unavailable")
);
if (connections.length === 0) {
console.log(t("test.noProviders"));
@@ -89,6 +88,7 @@ async function _runAllProviders(opts) {
}
const providers = connections.map((c) => ({
+ connectionId: c.id,
provider: c.provider ?? c.id,
model: c.defaultModel ?? c.model,
}));
@@ -102,8 +102,8 @@ async function _runAllProviders(opts) {
}
const results = await Promise.all(
- providers.map(async ({ provider, model }) => {
- const r = await _runSingleTest(provider, model);
+ providers.map(async ({ connectionId, provider, model }) => {
+ const r = await _runSingleTest({ id: connectionId }, model);
return { provider, model, ...r };
})
);
@@ -123,6 +123,13 @@ async function _runAllProviders(opts) {
async function _runCompare(provider, opts) {
const targetProvider = provider || "anthropic";
+ const connections = await _loadConnections();
+ if (!connections) return 1;
+ const connection = _resolveConnection(connections, targetProvider);
+ if (!connection) {
+ console.error(`Provider connection not found: ${targetProvider}`);
+ return 1;
+ }
const models = opts.compare
.split(",")
.map((m) => m.trim())
@@ -138,7 +145,7 @@ async function _runCompare(provider, opts) {
for (const model of models) {
const results = [];
for (let i = 0; i < repeat; i++) {
- const result = await _runSingleTest(targetProvider, model);
+ const result = await _runSingleTest(connection, model);
results.push(result);
}
rows.push({ model, ..._aggregate(results, true) });
@@ -180,19 +187,55 @@ async function _runCompare(provider, opts) {
return rows.every((r) => r.success) ? 0 : 1;
}
-async function _runSingleTest(provider, model) {
+async function _loadConnections() {
+ const res = await apiFetch("/api/providers?limit=200", {
+ retry: false,
+ timeout: 5000,
+ acceptNotOk: true,
+ });
+ if (!res.ok) {
+ console.error(t("test.noServer"));
+ return null;
+ }
+ const data = await res.json();
+ const connections = data.connections ?? data.providers ?? data.items ?? data;
+ if (!Array.isArray(connections)) {
+ console.error(t("test.noServer"));
+ return null;
+ }
+ return connections;
+}
+
+function _resolveConnection(connections, selector, model) {
+ const normalized = String(selector || "")
+ .trim()
+ .toLowerCase();
+ const active = connections.filter((connection) => connection.isActive !== false);
+ return (
+ active.find((connection) => String(connection.id || "").toLowerCase() === normalized) ??
+ active.find((connection) => String(connection.name || "").toLowerCase() === normalized) ??
+ active.find(
+ (connection) =>
+ String(connection.provider || "").toLowerCase() === normalized &&
+ (!model || connection.defaultModel === model || connection.model === model)
+ ) ??
+ active.find((connection) => String(connection.provider || "").toLowerCase() === normalized)
+ );
+}
+
+async function _runSingleTest(connection, model) {
const startMs = Date.now();
try {
- const res = await apiFetch("/api/v1/providers/test", {
+ const res = await apiFetch(`/api/providers/${encodeURIComponent(connection.id)}/test`, {
method: "POST",
- body: { provider, model },
+ body: model ? { validationModelId: model } : {},
retry: false,
timeout: 30000,
acceptNotOk: true,
});
const durationMs = Date.now() - startMs;
- const data = res.ok ? await res.json() : { success: false, error: `HTTP ${res.status}` };
- return { ...data, durationMs };
+ const data = res.ok ? await res.json() : { valid: false, error: `HTTP ${res.status}` };
+ return { ...data, success: data.valid === true, durationMs };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return {
diff --git a/bin/cli/tray/autostart.mjs b/bin/cli/tray/autostart.mjs
index 6c1ba21aee..a61a5c739a 100644
--- a/bin/cli/tray/autostart.mjs
+++ b/bin/cli/tray/autostart.mjs
@@ -121,7 +121,16 @@ function writeLinuxSystemdUnit(cliPath) {
"Wants=network-online.target",
"",
"[Service]",
- "Type=simple",
+ // Type=notify + WatchdogSec: the server sends READY=1 once listening and
+ // WATCHDOG=1 every 60s; if its event loop ever blocks (frozen process),
+ // the pings stop and systemd kills+restarts the service. NotifyAccess=all
+ // because the pings come from the server child, not the serve supervisor.
+ // Foreground serve only: `--daemon` escapes the cgroup and would break
+ // the notify handshake.
+ "Type=notify",
+ "NotifyAccess=all",
+ "WatchdogSec=180",
+ "TimeoutStartSec=300",
`ExecStart=${buildServeExecLine(cliPath, { tray: false })}`,
"Restart=on-failure",
"RestartSec=5",
diff --git a/bin/cli/tui/ProvidersTestAll.jsx b/bin/cli/tui/ProvidersTestAll.jsx
index 73fc78614a..c1911888ac 100644
--- a/bin/cli/tui/ProvidersTestAll.jsx
+++ b/bin/cli/tui/ProvidersTestAll.jsx
@@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback } from "react";
import { render, Box, Text, useInput } from "ink";
import Spinner from "ink-spinner";
+import { apiFetch } from "../api.mjs";
import { DataTable } from "../tui-components/DataTable.jsx";
import { ProgressBar } from "../tui-components/ProgressBar.jsx";
@@ -31,22 +32,20 @@ const TABLE_SCHEMA = [
{ key: "error", header: "Error", width: 28, formatter: (v) => (v ? v.slice(0, 26) : "") },
];
-async function testOne(provider, model, baseUrl, apiKey) {
- const headers = {
- "Content-Type": "application/json",
- ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
- };
+async function testOne(connectionId, model, baseUrl, apiKey) {
const start = Date.now();
try {
- const res = await fetch(`${baseUrl}/api/v1/providers/test`, {
+ const res = await apiFetch(`/api/providers/${encodeURIComponent(connectionId)}/test`, {
method: "POST",
- headers,
- body: JSON.stringify({ provider, model }),
- signal: AbortSignal.timeout(30000),
+ body: model ? { validationModelId: model } : {},
+ baseUrl,
+ token: apiKey,
+ timeout: 30000,
+ acceptNotOk: true,
});
const latencyMs = Date.now() - start;
- const data = res.ok ? await res.json() : { success: false, error: `HTTP ${res.status}` };
- return { status: data.success ? STATUS.PASS : STATUS.FAIL, latencyMs, error: data.error };
+ const data = res.ok ? await res.json() : { valid: false, error: `HTTP ${res.status}` };
+ return { status: data.valid ? STATUS.PASS : STATUS.FAIL, latencyMs, error: data.error };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return {
@@ -63,6 +62,7 @@ function ProvidersTestAllApp({ providers, baseUrl, apiKey, concurrency = 4, onEx
const [rows, setRows] = useState(() =>
providers.map((p, i) => ({
id: i,
+ connectionId: p.connectionId ?? p.id,
provider: p.provider ?? p.id ?? String(p),
model: p.model ?? p.defaultModel ?? "",
status: STATUS.PENDING,
@@ -91,7 +91,7 @@ function ProvidersTestAllApp({ providers, baseUrl, apiKey, concurrency = 4, onEx
const row = queue[cursor++];
running++;
update(row.id, { status: STATUS.RUNNING });
- testOne(row.provider, row.model, resolved, apiKey).then((result) => {
+ testOne(row.connectionId, row.model, resolved, apiKey).then((result) => {
update(row.id, result);
running--;
nextSlot();
diff --git a/bin/cli/utils/cliToken.mjs b/bin/cli/utils/cliToken.mjs
index 94691ba952..38895c13bf 100644
--- a/bin/cli/utils/cliToken.mjs
+++ b/bin/cli/utils/cliToken.mjs
@@ -12,25 +12,39 @@ function getActiveSalt() {
return process.env.OMNIROUTE_CLI_SALT || BUILTIN_DEFAULT_SALT;
}
-export async function getCliToken() {
- const salt = getActiveSalt();
- if (_cached !== null && _cachedSalt === salt) return _cached;
+export function deriveCliToken(machineIdModule, salt) {
try {
// node-machine-id is CommonJS: under `await import()` its exports land on
// `.default`, so destructuring `machineIdSync` off the namespace yields
// undefined and calling it throws — which the catch below turned into an
// empty token, silently disabling CLI auth for every management request.
// Same resolution order as src/lib/machineToken.ts.
- const mod = await import("node-machine-id");
- const machineIdSync = mod.machineIdSync ?? mod.default?.machineIdSync;
- if (typeof machineIdSync !== "function") throw new Error("machine-id API unavailable");
+ const machineIdSync =
+ machineIdModule?.machineIdSync || machineIdModule?.default?.machineIdSync;
+ if (typeof machineIdSync !== "function") return "";
// machineIdSync(true) returns the original unhashed hardware ID — mirrors
// getMachineTokenSync() in src/lib/machineToken.ts (#10148 cliToken hardening).
- const mid = machineIdSync(true);
- _cached = crypto.createHmac("sha256", mid).update(salt).digest("hex");
+ const rawId = machineIdSync(true);
+ if (!rawId) return "";
+ return crypto.createHmac("sha256", rawId).update(salt).digest("hex");
+ } catch {
+ return "";
+ }
+}
+
+export async function getCliToken() {
+ const salt = getActiveSalt();
+ if (_cached !== null && _cachedSalt === salt) return _cached;
+ try {
+ const imported = await import("node-machine-id");
+ const token = deriveCliToken(imported, salt);
+ if (!token) {
+ // Swallowing here changes control flow (every management call goes out
+ // unauthenticated and 401s), so leave a breadcrumb rather than failing mute.
+ console.debug("[CLI_TOKEN] machine-id resolution failed, CLI auth disabled");
+ }
+ _cached = token;
} catch (e) {
- // Swallowing here changes control flow (every management call goes out
- // unauthenticated and 401s), so leave a breadcrumb rather than failing mute.
console.debug("[CLI_TOKEN] machine-id resolution failed, CLI auth disabled:", e);
_cached = "";
}
diff --git a/bin/omniroute.mjs b/bin/omniroute.mjs
index c023879da8..fb0a455520 100755
--- a/bin/omniroute.mjs
+++ b/bin/omniroute.mjs
@@ -119,6 +119,9 @@ function loadEnvFile() {
addEnvPath(join(ROOT, ".env"));
}
+ const keyOrigin = new Map();
+ const shadowed = new Map();
+
for (const envPath of envPaths) {
try {
if (existsSync(envPath)) {
@@ -131,19 +134,31 @@ function loadEnvFile() {
const key = trimmed.slice(0, eqIdx).trim();
if (process.env[key] === undefined) {
process.env[key] = parseEnvValue(trimmed.slice(eqIdx + 1));
+ keyOrigin.set(key, envPath);
+ } else if (!shadowed.has(key)) {
+ // The line is inert: something set this key first. Report it once
+ // per key, whether the winner was an earlier file or the process
+ // environment (#6194: a shell's own HOSTNAME beat the .env and the
+ // server bound to the wrong address in silence).
+ shadowed.set(key, { winner: keyOrigin.get(key) ?? null, loser: envPath });
}
}
}
loadedEnvPaths.push(envPath);
}
- } catch {
- // Ignore errors reading env files.
+ } catch (err) {
+ console.warn(` \x1b[33m⚠ Could not read ${envPath}: ${err?.message ?? err}\x1b[0m`);
}
}
for (const envPath of loadedEnvPaths) {
console.log(` \x1b[2m📋 Loaded env from ${envPath}\x1b[0m`);
}
+
+ for (const [key, { winner, loser }] of shadowed) {
+ const setter = winner ? winner : "the environment";
+ console.warn(` \x1b[33m⚠ ${key} in ${loser} is ignored, ${setter} set it first\x1b[0m`);
+ }
}
loadEnvFile();
diff --git a/changelog.d/features/10303-healthz-event-loop-lag.md b/changelog.d/features/10303-healthz-event-loop-lag.md
new file mode 100644
index 0000000000..991c123021
--- /dev/null
+++ b/changelog.d/features/10303-healthz-event-loop-lag.md
@@ -0,0 +1 @@
+- **feat(resilience):** warn when `/healthz` is served under event-loop lag ≥200ms so a slow 200 is visible as sick, not healthy ([#10303](https://github.com/diegosouzapw/OmniRoute/issues/10303))
diff --git a/changelog.d/features/10316-livez-endpoint.md b/changelog.d/features/10316-livez-endpoint.md
new file mode 100644
index 0000000000..01409d7b48
--- /dev/null
+++ b/changelog.d/features/10316-livez-endpoint.md
@@ -0,0 +1 @@
+- **feat(docker):** add `GET`/`HEAD` `/livez` as a process-alive probe, distinct from `/healthz` readiness ([#10316](https://github.com/diegosouzapw/OmniRoute/issues/10316))
diff --git a/changelog.d/features/10587-ogg-speech-alias.md b/changelog.d/features/10587-ogg-speech-alias.md
new file mode 100644
index 0000000000..118e2a7b48
--- /dev/null
+++ b/changelog.d/features/10587-ogg-speech-alias.md
@@ -0,0 +1 @@
+- **feat(providers):** accept `response_format=ogg` on `/v1/audio/speech` as an alias for the existing Opus/Ogg encoder ([#10587](https://github.com/diegosouzapw/OmniRoute/issues/10587))
diff --git a/changelog.d/features/10662-systemd-notify.md b/changelog.d/features/10662-systemd-notify.md
new file mode 100644
index 0000000000..5a02e7df25
--- /dev/null
+++ b/changelog.d/features/10662-systemd-notify.md
@@ -0,0 +1 @@
+- feat(server): emit systemd sd_notify READY/WATCHDOG/STOPPING (generated unit becomes Type=notify with WatchdogSec=180) so a frozen server process is killed and restarted by systemd instead of lingering undetected
diff --git a/changelog.d/features/10668-newapi-gateway-protocols.md b/changelog.d/features/10668-newapi-gateway-protocols.md
new file mode 100644
index 0000000000..1ec6e5f0b7
--- /dev/null
+++ b/changelog.d/features/10668-newapi-gateway-protocols.md
@@ -0,0 +1,2 @@
+- **feat(providers):** add the TabiToken NewAPI gateway (`tabitoken`) and teach the existing HCNSec entry (`hcnsec`) the three further protocols it actually serves. TabiToken leaves the NewAPI pricing endpoint public, so its catalog is read from the host rather than guessed: four Claude models, each reporting the Anthropic and OpenAI protocols. HCNSec shipped OpenAI-only; probing the host showed `/v1/messages`, `/v1/responses` and the Gemini `/v1beta` path all reach its token layer, so each is now declared as an alternate format — with its default format, base URL, auth scheme and regional catalog classification untouched. ([#10668](https://github.com/diegosouzapw/OmniRoute/pull/10668)) — thanks @yawar-aquil
+- **feat(sse):** allow an alternate protocol to build its own upstream URL. `AlternateFormat` gained an optional `urlBuilder`, because the Gemini protocol carries the model inside the path (`{base}/{model}:generateContent`) and the existing `chatPath`/`urlSuffix` fields are constants that cannot express it. The route builder is extracted as `buildGeminiGenerateContentUrl` and shared with the native `gemini` provider so the two consumers cannot drift on the `?alt=sse` streaming suffix. ([#10668](https://github.com/diegosouzapw/OmniRoute/pull/10668)) — thanks @yawar-aquil
diff --git a/changelog.d/features/10670-call-logs-error-type.md b/changelog.d/features/10670-call-logs-error-type.md
new file mode 100644
index 0000000000..1ffd94bd22
--- /dev/null
+++ b/changelog.d/features/10670-call-logs-error-type.md
@@ -0,0 +1 @@
+- **feat(call_logs):** persist the per-call error family in `call_logs.error_type` and expose a failure breakdown (`errorBreakdown`) in the usage analytics endpoint, reusing the existing production classifier ([#10670](https://github.com/diegosouzapw/OmniRoute/issues/10670))
diff --git a/changelog.d/features/10677-egress-sharing-summary.md b/changelog.d/features/10677-egress-sharing-summary.md
new file mode 100644
index 0000000000..9e1f723a0d
--- /dev/null
+++ b/changelog.d/features/10677-egress-sharing-summary.md
@@ -0,0 +1 @@
+- **feat(proxy):** the proxy-health sweep and `GET /api/settings/proxies/egress` now report an anonymous summary of egress-IP sharing — how many rotation groups share an egress IP and the largest number of accounts behind one IP — computed from persisted `proxy_logs` over a 24h window. No IPs and no account identities by default; `PROXY_LOG_INCLUDE_IPS=true` restores raw details. ([#10677](https://github.com/diegosouzapw/OmniRoute/issues/10677))
diff --git a/changelog.d/features/10729-cursor-api-key-and-cli-passthrough.md b/changelog.d/features/10729-cursor-api-key-and-cli-passthrough.md
new file mode 100644
index 0000000000..96094ffff1
--- /dev/null
+++ b/changelog.d/features/10729-cursor-api-key-and-cli-passthrough.md
@@ -0,0 +1 @@
+- **feat(providers):** new `cursor-api` provider (card "Cursor API", alias `cua`): connect a Cursor user API key (`crsr_…`) and route `cursor-api/` through the existing Cursor agent executor (the key is exchanged for a 1h session token and cached), plus a `/api/cursor-cli/*` passthrough so the Cursor CLI itself runs through OmniRoute (`CURSOR_API_ENDPOINT=http:///api/cursor-cli`, `CURSOR_API_KEY=`) with every RPC attributed and logged. The IDE `cursor` provider is unchanged. (#10729)
diff --git a/changelog.d/features/10771-health-root-endpoint.md b/changelog.d/features/10771-health-root-endpoint.md
new file mode 100644
index 0000000000..a367bbce78
--- /dev/null
+++ b/changelog.d/features/10771-health-root-endpoint.md
@@ -0,0 +1 @@
+- **feat(api):** `GET /api/health` now answers `{ status, timestamp }` without a key. Until now the path had no route, so the management-auth boundary answered first with a 401 — indistinguishable from a wrong key or an unknown route, which left Docker HEALTHCHECKs and Kubernetes probes unable to tell "down" from "misconfigured". Kept deliberately minimal: version, uptime and memory stay behind the authenticated `/api/monitoring/health` ([#PRNUM](https://github.com/diegosouzapw/OmniRoute/pull/10771)).
diff --git a/changelog.d/features/10783-task-routing-configurable-patterns.md b/changelog.d/features/10783-task-routing-configurable-patterns.md
new file mode 100644
index 0000000000..e5c37c390a
--- /dev/null
+++ b/changelog.d/features/10783-task-routing-configurable-patterns.md
@@ -0,0 +1 @@
+- feat(routing): make Task-Aware Smart Routing's detection patterns operator-configurable via `settings.taskRouting.patternOverrides` (`PUT /api/settings/task-routing`) — the built-in patterns are English-only, so a non-English dashboard had no recourse short of turning detection off entirely; an override now replaces the pattern list for one task type without touching the rest (#10783)
diff --git a/changelog.d/features/10869-combo-patch-verb.md b/changelog.d/features/10869-combo-patch-verb.md
new file mode 100644
index 0000000000..f11893d95c
--- /dev/null
+++ b/changelog.d/features/10869-combo-patch-verb.md
@@ -0,0 +1 @@
+- feat(api): accept PATCH on /api/combos/[id], the verb the OpenAPI spec already documents (#10869)
diff --git a/changelog.d/features/10896-glm-5.3.md b/changelog.d/features/10896-glm-5.3.md
new file mode 100644
index 0000000000..0edfc4a55b
--- /dev/null
+++ b/changelog.d/features/10896-glm-5.3.md
@@ -0,0 +1 @@
+- **feat(sse):** add GLM-5.3 support (`glm-5.3`, `glm-5.3-high`, `glm-5.3-low`) across the z.ai first-party providers, mapping the upstream `reasoning_effort` request parameter to the existing 5.2 tier UX ([#10896](https://github.com/diegosouzapw/OmniRoute/pull/10896)) — thanks @phuongddx
diff --git a/changelog.d/features/10897-home-recent-requests.md b/changelog.d/features/10897-home-recent-requests.md
new file mode 100644
index 0000000000..fd6bcc9abe
--- /dev/null
+++ b/changelog.d/features/10897-home-recent-requests.md
@@ -0,0 +1 @@
+- **feat(home):** add a live **Recent Requests** panel beside the home Provider Topology (polls `GET /api/usage/call-logs?excludeTests=1` every ~3s, gated by the topology appearance toggle + page visibility). `excludeTests` is now an allowlist of real provider inference (`/v1/%` or `/api/v1/%`), applied before `LIMIT`, so connection-test/model-sync/management rows can never leak into the feed ([#10897](https://github.com/diegosouzapw/OmniRoute/pull/10897), extracted from [#8450](https://github.com/diegosouzapw/OmniRoute/pull/8450)) — thanks @nguyenha935
diff --git a/changelog.d/features/8443-credential-health-per-connection-interval.md b/changelog.d/features/8443-credential-health-per-connection-interval.md
new file mode 100644
index 0000000000..1fd1d3e5a2
--- /dev/null
+++ b/changelog.d/features/8443-credential-health-per-connection-interval.md
@@ -0,0 +1,2 @@
+- **feat(credential-health):** pace the credential health sweep per connection via `provider_connections.healthCheckInterval` (minutes, 0 = never), with `CREDENTIAL_HEALTH_CHECK_INTERVAL` as the global default ([#8443](https://github.com/diegosouzapw/OmniRoute/issues/8443))
+- **behavior change:** `healthCheckInterval` is a shared column — it paces both the OAuth token refresh and the credential health sweep, and `0` disables both. The connection editor defaults it to 60, so configured OAuth connections are now credential-checked at 60min instead of the previous ~10min (aligned with the probe-volume goal of #8443)
diff --git a/changelog.d/features/cursor-agent-image-provider.md b/changelog.d/features/cursor-agent-image-provider.md
new file mode 100644
index 0000000000..84646dc44e
--- /dev/null
+++ b/changelog.d/features/cursor-agent-image-provider.md
@@ -0,0 +1 @@
+- feat(sse): add Cursor plan image generation via Agent CLI (`IMAGE_PROVIDERS.cursor`, format `cursor-agent-image`), reusing the chat Cursor OAuth connection
diff --git a/changelog.d/features/disable-context-window-checks.md b/changelog.d/features/disable-context-window-checks.md
new file mode 100644
index 0000000000..1cdd3cc0a8
--- /dev/null
+++ b/changelog.d/features/disable-context-window-checks.md
@@ -0,0 +1 @@
+- feat(routing): add the default-off `DISABLE_CONTEXT_WINDOW_CHECKS` feature flag to let operators bypass OmniRoute's local context-window and max-input-token check for direct single-model requests, leaving upstream limits, prompt compression, and output-token caps intact.
diff --git a/changelog.d/features/kimi-coding-extra-usage.md b/changelog.d/features/kimi-coding-extra-usage.md
new file mode 100644
index 0000000000..766ec1020c
--- /dev/null
+++ b/changelog.d/features/kimi-coding-extra-usage.md
@@ -0,0 +1 @@
+- **feat(usage):** show Kimi Coding's fixed-order Code 5-hour/7-day quota windows plus Extra Usage status, balance, monthly spend/limit, and the official Additional Credits link on Dashboard → Quota cards.
diff --git a/changelog.d/fixes/10095-antigravity-multiaccount-quota-false-exhaustion.md b/changelog.d/fixes/10095-antigravity-multiaccount-quota-false-exhaustion.md
new file mode 100644
index 0000000000..579005e943
--- /dev/null
+++ b/changelog.d/fixes/10095-antigravity-multiaccount-quota-false-exhaustion.md
@@ -0,0 +1 @@
+- fix(domain): stop treating an unreported Antigravity quota fraction (`fractionReported:false`) as 0% remaining in `quotaCache.ts`, which was falsely marking every fresh/newly-connected account as exhausted and blocking multi-account rotation (#10095)
diff --git a/changelog.d/fixes/10156-responses-commentary-completed-snapshot.md b/changelog.d/fixes/10156-responses-commentary-completed-snapshot.md
new file mode 100644
index 0000000000..7a976ab85d
--- /dev/null
+++ b/changelog.d/fixes/10156-responses-commentary-completed-snapshot.md
@@ -0,0 +1 @@
+- **fix(sse):** Responses-passthrough `response.completed` snapshots now drop `phase:"commentary"` items the same way live SSE frames already do, so the terminal `response.output` array no longer echoes internal commentary text that was already suppressed from the stream (#10156).
diff --git a/changelog.d/fixes/10162-approximate-combo-context-advisory.md b/changelog.d/fixes/10162-approximate-combo-context-advisory.md
new file mode 100644
index 0000000000..3c1bc703a0
--- /dev/null
+++ b/changelog.d/fixes/10162-approximate-combo-context-advisory.md
@@ -0,0 +1 @@
+- **fix(routing):** keep approximate Combo context estimates advisory so requests reach concrete targets instead of returning a pre-dispatch 400 ([#10162](https://github.com/diegosouzapw/OmniRoute/pull/10162)) — thanks @xz-dev
diff --git a/changelog.d/fixes/10345-bare-combo-opencode-ids.md b/changelog.d/fixes/10345-bare-combo-opencode-ids.md
new file mode 100644
index 0000000000..c3a6a499ec
--- /dev/null
+++ b/changelog.d/fixes/10345-bare-combo-opencode-ids.md
@@ -0,0 +1 @@
+- **fix(opencode-plugin):** publish bare combo model ids without the plugin provider prefix so OpenCode can select them ([#10345](https://github.com/diegosouzapw/OmniRoute/issues/10345))
diff --git a/changelog.d/fixes/10346-empty-pool-warn-once.md b/changelog.d/fixes/10346-empty-pool-warn-once.md
new file mode 100644
index 0000000000..e4b50ef3ff
--- /dev/null
+++ b/changelog.d/fixes/10346-empty-pool-warn-once.md
@@ -0,0 +1 @@
+- **fix(backend):** log `auto/ matched no connected models` once per process per label instead of every minute ([#10346](https://github.com/diegosouzapw/OmniRoute/issues/10346))
diff --git a/changelog.d/fixes/10353-memory-heap-conflict-warn.md b/changelog.d/fixes/10353-memory-heap-conflict-warn.md
new file mode 100644
index 0000000000..c52b7cc15c
--- /dev/null
+++ b/changelog.d/fixes/10353-memory-heap-conflict-warn.md
@@ -0,0 +1 @@
+- **fix(docker):** warn at boot when `OMNIROUTE_MEMORY_MB` disagrees with `NODE_OPTIONS --max-old-space-size`, and document that the standalone/Docker launcher appends `OMNIROUTE_MEMORY_MB` last ([#10353](https://github.com/diegosouzapw/OmniRoute/issues/10353))
diff --git a/changelog.d/fixes/10470-antigravity-byop-account-rotation.md b/changelog.d/fixes/10470-antigravity-byop-account-rotation.md
new file mode 100644
index 0000000000..9ec58e152a
--- /dev/null
+++ b/changelog.d/fixes/10470-antigravity-byop-account-rotation.md
@@ -0,0 +1 @@
+- **fix(antigravity):** automatically rotate to a sibling account when one is BYOP (GCP Project ID required, `gcp_project_required` 422) — the account is excluded from selection for 24h and the request succeeds via another account instead of failing fast; the actionable 422 is surfaced only when no sibling exists (follow-up to the #10424 BYOP fast-fail) ([#10470](https://github.com/diegosouzapw/OmniRoute/pull/10470)) — thanks @rqzbeh
diff --git a/changelog.d/fixes/10528-direct-dispatcher-response-start-timeout.md b/changelog.d/fixes/10528-direct-dispatcher-response-start-timeout.md
new file mode 100644
index 0000000000..9354b02822
--- /dev/null
+++ b/changelog.d/fixes/10528-direct-dispatcher-response-start-timeout.md
@@ -0,0 +1 @@
+- **fix(network):** direct (no-proxy) egress now bounds each attempt's response-start window (default 30s, `OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS`) and retries once on a fresh no-keep-alive socket, so a silently-dropped pooled keep-alive connection can no longer stall direct providers (opencode-go, command-code) until a service restart ([#10214](https://github.com/diegosouzapw/OmniRoute/issues/10214))
diff --git a/changelog.d/fixes/10536-llmlingua-2-2.0.5-drop-tfjs.md b/changelog.d/fixes/10536-llmlingua-2-2.0.5-drop-tfjs.md
new file mode 100644
index 0000000000..11a1845715
--- /dev/null
+++ b/changelog.d/fixes/10536-llmlingua-2-2.0.5-drop-tfjs.md
@@ -0,0 +1 @@
+- **fix(deps):** upgrade `@atjsh/llmlingua-2` from 2.0.3 to 2.0.5 and remove `@tensorflow/tfjs` from the LLMLingua SLM stack — 2.0.5 adds official Transformers.js v4 support (peers `@huggingface/transformers` at `^3.5.2 || ^4.0.0`) and 2.0.4+ no longer requires TensorFlow.js, restoring compatibility with OmniRoute's Transformers.js v4 while dropping the largest single contributor to the optional runtime footprint ([#10536](https://github.com/diegosouzapw/OmniRoute/issues/10536))
diff --git a/changelog.d/fixes/10550-responses-reasoning-transport.md b/changelog.d/fixes/10550-responses-reasoning-transport.md
new file mode 100644
index 0000000000..d34c433deb
--- /dev/null
+++ b/changelog.d/fixes/10550-responses-reasoning-transport.md
@@ -0,0 +1 @@
+- Preserve portable plaintext reasoning by default across streaming and non-streaming Chat Completions and Responses routes while keeping provider-bound opaque state target-compatible. Combos now drop incompatible continuation reasoning by default and can explicitly skip incompatible targets, while known providers no longer show redundant encrypted-reasoning controls. (#10550)
diff --git a/changelog.d/fixes/10592-playground-chattab-endpoint-routing.md b/changelog.d/fixes/10592-playground-chattab-endpoint-routing.md
new file mode 100644
index 0000000000..ca602b9122
--- /dev/null
+++ b/changelog.d/fixes/10592-playground-chattab-endpoint-routing.md
@@ -0,0 +1 @@
+- fix(dashboard): route the Playground's ChatTab "Send" through the endpoint actually selected in StudioConfigPane (`search`, `web.fetch`, etc.) instead of always POSTing to `/api/v1/chat/completions`, fixing the false "No active credentials for provider" 404 when testing search-only providers (#10592)
diff --git a/changelog.d/fixes/10594-freepik-magnific-api.md b/changelog.d/fixes/10594-freepik-magnific-api.md
new file mode 100644
index 0000000000..4c1c59a701
--- /dev/null
+++ b/changelog.d/fixes/10594-freepik-magnific-api.md
@@ -0,0 +1 @@
+- **fix(providers):** Magnific Mystic is now the canonical provider (`/dashboard/providers/magnific`, `magnific/`). It uses the Magnific API (`api.magnific.com` + `x-magnific-api-key`), dashboard Test Connection validates keys without starting a paid generation, and the old `freepik` slug remains a legacy alias ([#10594](https://github.com/diegosouzapw/OmniRoute/pull/10594))
diff --git a/changelog.d/fixes/10597-combo-log-error-body.md b/changelog.d/fixes/10597-combo-log-error-body.md
new file mode 100644
index 0000000000..ff6608947c
--- /dev/null
+++ b/changelog.d/fixes/10597-combo-log-error-body.md
@@ -0,0 +1 @@
+- **fix(sse):** Include the redacted upstream error body in the per-target COMBO failure log (`Model X failed, trying next`) so operators can triage a 400/500 without reproducing the request ([#10597](https://github.com/diegosouzapw/OmniRoute/issues/10597))
diff --git a/changelog.d/fixes/10686-combo-quota-token-limit-await.md b/changelog.d/fixes/10686-combo-quota-token-limit-await.md
new file mode 100644
index 0000000000..a9e7b910e3
--- /dev/null
+++ b/changelog.d/fixes/10686-combo-quota-token-limit-await.md
@@ -0,0 +1 @@
+- **Combo routing:** await each connection's token limit before reserving quota. The old lookup treated the `Promise` as a connection and dropped `rateLimitOverrides.tpm` ([#10686](https://github.com/diegosouzapw/OmniRoute/pull/10686)).
diff --git a/changelog.d/fixes/10732-copilot-m365-invocation-refresh.md b/changelog.d/fixes/10732-copilot-m365-invocation-refresh.md
new file mode 100644
index 0000000000..acbfbbe693
--- /dev/null
+++ b/changelog.d/fixes/10732-copilot-m365-invocation-refresh.md
@@ -0,0 +1 @@
+- **fix(providers):** copilot-m365-web chat turns no longer surface as `(empty response)` — the type:4 invocation is aligned with the 2026-08 wire shape and now carries its type:1 Metrics follow-up in the same socket write, and the access token pre-flight-refreshes from a stored refresh_token instead of requiring a DevTools re-capture every ~75 minutes ([#10732](https://github.com/diegosouzapw/OmniRoute/pull/10732) — thanks @acc0mplish)
diff --git a/changelog.d/fixes/10734-combo-context-generic-default.md b/changelog.d/fixes/10734-combo-context-generic-default.md
new file mode 100644
index 0000000000..988c435d4e
--- /dev/null
+++ b/changelog.d/fixes/10734-combo-context-generic-default.md
@@ -0,0 +1 @@
+- **fix(catalog):** stop counting `getTokenLimit()`'s generic 128k catch-all as a known combo window, so `/v1/models` advertises the min of sourced member contexts instead of collapsing a 500k combo to 128k ([#10734](https://github.com/diegosouzapw/OmniRoute/issues/10734))
diff --git a/changelog.d/fixes/10735-search-provider-named-errors.md b/changelog.d/fixes/10735-search-provider-named-errors.md
new file mode 100644
index 0000000000..0e82f36aa8
--- /dev/null
+++ b/changelog.d/fixes/10735-search-provider-named-errors.md
@@ -0,0 +1 @@
+- **fix(search):** name `/v1/search` 502s with provider id and sanitized Node cause code, without hostnames ([#10735](https://github.com/diegosouzapw/OmniRoute/issues/10735))
diff --git a/changelog.d/fixes/10765-rtk-unconditional-stats-cpu.md b/changelog.d/fixes/10765-rtk-unconditional-stats-cpu.md
new file mode 100644
index 0000000000..ff36462d47
--- /dev/null
+++ b/changelog.d/fixes/10765-rtk-unconditional-stats-cpu.md
@@ -0,0 +1 @@
+- fix(compression): skip the expensive `createCompressionStats()` pass in RTK when no message was actually compressed, matching every sibling stacked engine (#10765)
diff --git a/changelog.d/fixes/10769-cache-stats-real-cache.md b/changelog.d/fixes/10769-cache-stats-real-cache.md
new file mode 100644
index 0000000000..baaacc660d
--- /dev/null
+++ b/changelog.d/fixes/10769-cache-stats-real-cache.md
@@ -0,0 +1 @@
+- **fix(api):** `/api/cache/stats` reported the prompt-cache LRU, which no request path ever writes to — it answered `0 hit / 0 miss, size 0` while the semantic cache served real traffic, and the Health and Usage dashboards rendered that as fact. It now reports the semantic cache's in-memory entries, with the same response shape ([#PRNUM](https://github.com/diegosouzapw/OmniRoute/pull/10769)) — thanks @Poid-ZA, who first fixed this in #9446.
diff --git a/changelog.d/fixes/10770-console-interceptor-message-fidelity.md b/changelog.d/fixes/10770-console-interceptor-message-fidelity.md
new file mode 100644
index 0000000000..c35260f36a
--- /dev/null
+++ b/changelog.d/fixes/10770-console-interceptor-message-fidelity.md
@@ -0,0 +1 @@
+- **fix(logging):** the app log is filterable and readable again. Entries from the tagged logger (`[LEVEL] [TAG] message`) were filed under the level instead of the component, and printf format strings were never applied, so `%s`/`%d` stayed literal with the values trailing behind them unlabelled — including every LiveWS connection line, where the format is deliberate hardening against injected format specifiers ([#PRNUM](https://github.com/diegosouzapw/OmniRoute/pull/10770)).
diff --git a/changelog.d/fixes/10774-claude-code-flat-rate.md b/changelog.d/fixes/10774-claude-code-flat-rate.md
new file mode 100644
index 0000000000..ea09e2b208
--- /dev/null
+++ b/changelog.d/fixes/10774-claude-code-flat-rate.md
@@ -0,0 +1 @@
+- **fix(analytics):** Claude Code (`claude`/`cc`) is a flat-rate subscription, so cost analytics reports `$0` for it instead of estimating Anthropic list prices — the metered `anthropic` API keeps its real cost, and budget/quota/routing still estimate as before ([#10774](https://github.com/diegosouzapw/OmniRoute/pull/10774)) — thanks @electrumguy
diff --git a/changelog.d/fixes/10781-wal-truncate-scheduler.md b/changelog.d/fixes/10781-wal-truncate-scheduler.md
new file mode 100644
index 0000000000..4eb13a271b
--- /dev/null
+++ b/changelog.d/fixes/10781-wal-truncate-scheduler.md
@@ -0,0 +1 @@
+- fix(db): periodically run `wal_checkpoint(TRUNCATE)` so the SQLite WAL file shrinks on long-running servers (default 6h, override with `OMNIROUTE_WAL_TRUNCATE_INTERVAL_MS`, `0` disables) (#10781)
diff --git a/changelog.d/fixes/10782-ws-heartbeat-ping-pong.md b/changelog.d/fixes/10782-ws-heartbeat-ping-pong.md
new file mode 100644
index 0000000000..23aeaf3d3a
--- /dev/null
+++ b/changelog.d/fixes/10782-ws-heartbeat-ping-pong.md
@@ -0,0 +1 @@
+- fix(sse): replace LiveWS's application-only liveness check with a protocol-level `ws.ping()`/`pong` heartbeat (RFC 6455 §5.5.2) alongside the existing one, so a read-only dashboard subscriber that never sends anything survives the connection timeout — a socket that stops reading frames entirely is still reaped exactly as before (#10782)
diff --git a/changelog.d/fixes/10788-ollama-cloud-effort-tiers.md b/changelog.d/fixes/10788-ollama-cloud-effort-tiers.md
new file mode 100644
index 0000000000..0437576d38
--- /dev/null
+++ b/changelog.d/fixes/10788-ollama-cloud-effort-tiers.md
@@ -0,0 +1 @@
+- **fix(open-sse):** declare `supportedThinkingEfforts` (`low`/`medium`/`high`/`max`) on Ollama Cloud's `glm-5.1`, `glm-5.2`, `deepseek-v4-pro` and `deepseek-v4-flash` registry entries so the catalog's `appendSyncedEffortVariants()` pass — which only synthesizes selectable `-low`/`-high`/`-max` model ids from an already-populated `capabilities.effort_tiers` — can expose an effort selector for these reasoning-capable models, matching what `gpt-oss:20b`/`gpt-oss:120b` already had (#10788)
diff --git a/changelog.d/fixes/10792-double-transport-retry-scope.md b/changelog.d/fixes/10792-double-transport-retry-scope.md
new file mode 100644
index 0000000000..337b680add
--- /dev/null
+++ b/changelog.d/fixes/10792-double-transport-retry-scope.md
@@ -0,0 +1 @@
+- **fix(resilience):** scope the same-account transport retry (#9708) out of emergency-fallback and combo hops — it was retrying the free fallback model and combo targets too, doubling upstream calls and corrupting the terminal error status on those paths.
diff --git a/changelog.d/fixes/10799-provider-health-inconclusive-probes.md b/changelog.d/fixes/10799-provider-health-inconclusive-probes.md
new file mode 100644
index 0000000000..72aacacee0
--- /dev/null
+++ b/changelog.d/fixes/10799-provider-health-inconclusive-probes.md
@@ -0,0 +1 @@
+- **fix(providers):** Keep NVIDIA timeout probes and generic Antigravity/AGY HTTP 400 probes from poisoning credential health while preserving explicit Google geo-block handling ([#10799](https://github.com/diegosouzapw/OmniRoute/pull/10799)) — thanks @Zartharas
diff --git a/changelog.d/fixes/10815-kiro-oauth-profilearn-dedup.md b/changelog.d/fixes/10815-kiro-oauth-profilearn-dedup.md
new file mode 100644
index 0000000000..51768aab4c
--- /dev/null
+++ b/changelog.d/fixes/10815-kiro-oauth-profilearn-dedup.md
@@ -0,0 +1 @@
+- fix(db): disambiguate `createProviderConnection()`'s OAuth email dedup by `providerSpecificData.profileArn` in addition to `username`, so adding a second Kiro/AWS profile with the same email creates a new connection instead of silently merging into the first (#10815)
diff --git a/changelog.d/fixes/10832-unprefixed-dalle3.md b/changelog.d/fixes/10832-unprefixed-dalle3.md
new file mode 100644
index 0000000000..2dfd970b13
--- /dev/null
+++ b/changelog.d/fixes/10832-unprefixed-dalle3.md
@@ -0,0 +1 @@
+- **fix(images):** register OpenAI `dall-e-3` in the image registry so unprefixed `dall-e-3` (and `openai/dall-e-3`) route to OpenAI Images instead of Microsoft Designer Web, and so the chat catalog no longer lists `openai/dall-e-3` as a 128k chat model ([#10832](https://github.com/diegosouzapw/OmniRoute/issues/10832))
diff --git a/changelog.d/fixes/10843-outbound-guard-mapped-ipv4.md b/changelog.d/fixes/10843-outbound-guard-mapped-ipv4.md
new file mode 100644
index 0000000000..2894ff65b0
--- /dev/null
+++ b/changelog.d/fixes/10843-outbound-guard-mapped-ipv4.md
@@ -0,0 +1 @@
+- **fix(security):** Outbound URL guard now resolves IPv4-mapped IPv6 literals to their embedded address, so `[::ffff:169.254.169.254]` is refused by the unconditional cloud-metadata block like its dotted spelling; `[::]` is refused alongside `0.0.0.0` ([#10843](https://github.com/diegosouzapw/OmniRoute/pull/10843)) — thanks @ntdat812
diff --git a/changelog.d/fixes/10848-image-scan-cookie-bridge.md b/changelog.d/fixes/10848-image-scan-cookie-bridge.md
new file mode 100644
index 0000000000..07e0f20291
--- /dev/null
+++ b/changelog.d/fixes/10848-image-scan-cookie-bridge.md
@@ -0,0 +1 @@
+- fix(config): exclude cookie-auth image bridges (chatgpt-web, gemini-web) from the unprefixed model scan so a bare id never silently binds to an unofficial web bridge (#10848)
diff --git a/changelog.d/fixes/10849-search-provider-opaque-400.md b/changelog.d/fixes/10849-search-provider-opaque-400.md
new file mode 100644
index 0000000000..a8982fb194
--- /dev/null
+++ b/changelog.d/fixes/10849-search-provider-opaque-400.md
@@ -0,0 +1 @@
+- fix(api): POST /v1/search now replies with a named `Unknown search provider: ` error (and field-named validation messages) instead of an opaque `Invalid request` for unrecognized or short-alias provider ids like `brave`/`serper` (#10849)
diff --git a/changelog.d/fixes/10853-i18n-disabled-mistranslation.md b/changelog.d/fixes/10853-i18n-disabled-mistranslation.md
new file mode 100644
index 0000000000..836cc6491e
--- /dev/null
+++ b/changelog.d/fixes/10853-i18n-disabled-mistranslation.md
@@ -0,0 +1 @@
+- **fix(i18n):** The "Disabled" status no longer renders as the noun for a person with a disability in Japanese, Spanish, Hindi, Polish, Telugu, Urdu and both Chinese locales — 24 strings now use each catalog's existing wording (ja 無効, es Deshabilitado, hi अक्षम, pl Wyłączone, te నిలిపివేయబడింది, ur غیر فعال, zh-CN 已禁用, zh-TW 已停用) ([#10812](https://github.com/diegosouzapw/OmniRoute/issues/10812), [#10853](https://github.com/diegosouzapw/OmniRoute/pull/10853)) — thanks @ntdat812
diff --git a/changelog.d/fixes/10857-hide-auto-models-when-routing-disabled.md b/changelog.d/fixes/10857-hide-auto-models-when-routing-disabled.md
new file mode 100644
index 0000000000..39da596ee3
--- /dev/null
+++ b/changelog.d/fixes/10857-hide-auto-models-when-routing-disabled.md
@@ -0,0 +1 @@
+- **fix(catalog):** `/v1/models` no longer advertises the built-in `auto/*` ids while auto routing is disabled — they were listed but rejected at request time with `Auto routing is disabled` ([#10831](https://github.com/diegosouzapw/OmniRoute/issues/10831), [#10857](https://github.com/diegosouzapw/OmniRoute/pull/10857)) — thanks @ntdat812
diff --git a/changelog.d/fixes/10858-base64-file-token-estimate.md b/changelog.d/fixes/10858-base64-file-token-estimate.md
new file mode 100644
index 0000000000..18d8104b10
--- /dev/null
+++ b/changelog.d/fixes/10858-base64-file-token-estimate.md
@@ -0,0 +1 @@
+- **fix(context):** Base64 file payloads (OpenAI `file` parts, Responses `input_file`, Claude `document` blocks) are budgeted like the Gemini `inlineData` path instead of being counted as prompt text — a ~1MB PDF estimated at 350k tokens and was rejected on the context limit before reaching the provider's document pipeline ([#10840](https://github.com/diegosouzapw/OmniRoute/issues/10840), [#10858](https://github.com/diegosouzapw/OmniRoute/pull/10858)) — thanks @ntdat812
diff --git a/changelog.d/fixes/10860-mcp-upstream-fetch-timeout.md b/changelog.d/fixes/10860-mcp-upstream-fetch-timeout.md
new file mode 100644
index 0000000000..a23aeed2a1
--- /dev/null
+++ b/changelog.d/fixes/10860-mcp-upstream-fetch-timeout.md
@@ -0,0 +1 @@
+- **fix(mcp):** MCP tool calls that wait on a model provider no longer abort after 10 seconds. `omniRouteFetch` applied a single hardcoded `AbortSignal.timeout(10000)` to every internal hop, and `omniroute_route_request` — which posts to `/v1/chat/completions` and waits on the upstream provider, plus auto-combo candidate probing before a provider is even chosen — passed no signal of its own, so it inherited it. Any route slower than 10s failed from the MCP side while the identical request succeeded through the REST API. `omniroute_web_search` and `omniroute_web_fetch` in the same file already carried an explicit 60s signal, so that value is now shared by all three provider-bound calls instead of being repeated as a literal, while management reads (health, resilience, rate limits, combos, quota, usage) keep their fast-fail 10s budget so a stalled local endpoint still cannot hold a tool call open. Both budgets are overridable through `OMNIROUTE_MCP_FETCH_TIMEOUT_MS` and `OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS`, replacing the reported workaround of patching the compiled `dist/.build/next/server/chunks/*.js`; a malformed or non-positive override falls back to the default rather than disabling the timeout
diff --git a/changelog.d/fixes/10862-sync-models-degraded-cached-catalog.md b/changelog.d/fixes/10862-sync-models-degraded-cached-catalog.md
new file mode 100644
index 0000000000..fd6f7d3f04
--- /dev/null
+++ b/changelog.d/fixes/10862-sync-models-degraded-cached-catalog.md
@@ -0,0 +1 @@
+- **fix(providers):** importing models with an expired API key now surfaces the credential error instead of reporting "No new models were added". The Import button posts to `/api/providers/{id}/sync-models`, which self-fetches the models route; that route does not fail on an upstream 401 but degrades to a catalog it already has, preferring the cache and using the local catalog only when there is no cache. A provider that imported successfully once therefore has a cache, so an expired key produced `{ source: "cache", warning: "Models probe failed (401) — using cached catalog" }` with HTTP 200 — and the #5460/#5465 degradation guard only recognised the `local_catalog` branch, so model-sync accepted it as a successful discovery, found every cached model already imported, and returned the empty-diff result. Retest does not go through this path, which is why it failed correctly and made the import look like a genuine "nothing to do". The existing rule — a degraded discovery must not be persisted as the synced catalog — is now applied to the branch it missed rather than special-casing 401/403, discriminating on the warning the fallback builder always attaches (an ordinary non-refresh cache hit attaches none, and model-sync always requests `refresh=true`). `isDegradedLocalCatalog` keeps its exact meaning and its existing tests
diff --git a/changelog.d/fixes/10866-combo-empty-models.md b/changelog.d/fixes/10866-combo-empty-models.md
new file mode 100644
index 0000000000..e71092d5d4
--- /dev/null
+++ b/changelog.d/fixes/10866-combo-empty-models.md
@@ -0,0 +1 @@
+- fix(api): reject a combo update that removes every model, and store the copilot's combo targets where the router reads them (#10866)
diff --git a/changelog.d/fixes/10868-proxy-echo-ipv4-fallback.md b/changelog.d/fixes/10868-proxy-echo-ipv4-fallback.md
new file mode 100644
index 0000000000..91f4e717e8
--- /dev/null
+++ b/changelog.d/fixes/10868-proxy-echo-ipv4-fallback.md
@@ -0,0 +1 @@
+- **fix(proxy):** proxy "Test connection" no longer reports an IPv4-only SOCKS5/SSH proxy as dead. #1255 moved every egress probe from `api.ipify.org` to `api64.ipify.org` so proxies with IPv6 egress could be tested, but `api64` is IPv6-first: a tunnel with no IPv6 route has nothing to connect to, so the probe hung until the caller's deadline and a proxy that was carrying live LLM traffic came back as a failure. Swapping the target to `api4` fixes that case and re-breaks the one #1255 fixed, so the probe now tries the targets in order instead — `api64` first, so a proxy with working IPv6 answers on the first attempt and keeps the exact behaviour #1255 introduced, including which of its addresses is reported (the egress IP is used as an identity to detect accounts of one rotation group sharing an address, so the attempts are sequential rather than raced). The attempts split the budget each call site already enforced, so no probe can take longer than it could before, and each attempt gets its own `AbortController` so exhausting the budget on an unreachable target does not abort the next one. `OMNIROUTE_PROXY_ECHO_URL` pins a single target — including a self-hosted echo — replacing the workaround of rewriting the compiled bundle after every upgrade. The relay branch of the test route still targets `api64` through `x-relay-target`, since that request egresses from the relay worker rather than the operator's tunnel
diff --git a/changelog.d/fixes/10870-cli-env-collision.md b/changelog.d/fixes/10870-cli-env-collision.md
new file mode 100644
index 0000000000..95a428ba08
--- /dev/null
+++ b/changelog.d/fixes/10870-cli-env-collision.md
@@ -0,0 +1 @@
+- fix(cli): warn when a .env line never takes effect, and stop swallowing an unreadable .env (#10870)
diff --git a/changelog.d/fixes/10873-mimocode-retirement-state-cleanup.md b/changelog.d/fixes/10873-mimocode-retirement-state-cleanup.md
new file mode 100644
index 0000000000..44443eac6c
--- /dev/null
+++ b/changelog.d/fixes/10873-mimocode-retirement-state-cleanup.md
@@ -0,0 +1 @@
+- **fix(db):** Remove stale MiMoCode provider configuration, including the legacy `mcode` alias, left after provider retirement while preserving historical usage and call logs ([#10873](https://github.com/diegosouzapw/OmniRoute/pull/10873)) — thanks @Zartharas
diff --git a/changelog.d/fixes/10877-quota-alias-fetcher-lookup-gap.md b/changelog.d/fixes/10877-quota-alias-fetcher-lookup-gap.md
new file mode 100644
index 0000000000..9501c6dad2
--- /dev/null
+++ b/changelog.d/fixes/10877-quota-alias-fetcher-lookup-gap.md
@@ -0,0 +1 @@
+- **fix(sse):** `getResetAwareProvider()` and the auto-combo quota lookup in `combo.ts` now canonicalize the provider id via `resolveProviderId()` before calling `getQuotaFetcher()`, so a fetcher registered under a provider's canonical id (e.g. `ollama-cloud`, `codex`) is found for combo targets stored under an alias spelling (e.g. `ollamacloud`, `cx`) instead of silently degrading reset-aware/reset-window/auto quota-aware routing to plain priority ordering (#10877)
diff --git a/changelog.d/fixes/10878-unsupported-validation-probes-neutral.md b/changelog.d/fixes/10878-unsupported-validation-probes-neutral.md
new file mode 100644
index 0000000000..1fc7c01933
--- /dev/null
+++ b/changelog.d/fixes/10878-unsupported-validation-probes-neutral.md
@@ -0,0 +1 @@
+- **fix(provider-health):** Keep unsupported 404/405 validation probes neutral so they do not poison stored credential health or scheduler failure state, while still honoring per-connection health-check pacing ([#10878](https://github.com/diegosouzapw/OmniRoute/pull/10878)) — thanks @Zartharas
diff --git a/changelog.d/fixes/10882-antigravity-gemini37-flash-tiers.md b/changelog.d/fixes/10882-antigravity-gemini37-flash-tiers.md
new file mode 100644
index 0000000000..b1ff4bbf5a
--- /dev/null
+++ b/changelog.d/fixes/10882-antigravity-gemini37-flash-tiers.md
@@ -0,0 +1 @@
+- **fix(antigravity):** map Gemini 3.7 Flash tier ids (`gemini-3.7-flash-high/medium/low`, bare `gemini-3.7-flash`) to the upstream `gemini-3.7-flash-tiered` model id Google's Cloud Code endpoint expects, and configure per-tier thinking budgets ([#10882](https://github.com/diegosouzapw/OmniRoute/pull/10882)) — thanks @adevwithpurpose
diff --git a/changelog.d/fixes/10887-memory-mcp-tools.md b/changelog.d/fixes/10887-memory-mcp-tools.md
new file mode 100644
index 0000000000..8dc02db1d9
--- /dev/null
+++ b/changelog.d/fixes/10887-memory-mcp-tools.md
@@ -0,0 +1 @@
+- **fix(memory):** enable agent memory save/update via MCP tools (`memory_save`/`update`/`search`/`delete` builtins with per-provider schemas, `apiKeyId` optional with caller-principal fallback) and gate server-side memory builtin injection to non-stream requests only ([#10887](https://github.com/diegosouzapw/OmniRoute/pull/10887)) — thanks @Egorich-print
diff --git a/changelog.d/fixes/10902-pplx-search-hint-optin.md b/changelog.d/fixes/10902-pplx-search-hint-optin.md
new file mode 100644
index 0000000000..233fb61f19
--- /dev/null
+++ b/changelog.d/fixes/10902-pplx-search-hint-optin.md
@@ -0,0 +1 @@
+- **fix(perplexity-web):** make the built-in-search hint appended to every system message opt-in via `OMNIROUTE_PPLX_SEARCH_HINT` (off by default) — Perplexity's answer engine searches anyway, and the hint leaked into replies as meta-commentary for coding clients ([#10902](https://github.com/diegosouzapw/OmniRoute/pull/10902), extracted from [#8634](https://github.com/diegosouzapw/OmniRoute/pull/8634)) — thanks @danscMax
diff --git a/changelog.d/fixes/8307-codex-image-account-fallback-retryable.md b/changelog.d/fixes/8307-codex-image-account-fallback-retryable.md
new file mode 100644
index 0000000000..bd4a70a1ab
--- /dev/null
+++ b/changelog.d/fixes/8307-codex-image-account-fallback-retryable.md
@@ -0,0 +1 @@
+- **fix(images):** retry Codex image generation on a sibling ChatGPT account when the requested model isn't entitled on the current account, instead of failing the request outright ([#8307](https://github.com/diegosouzapw/OmniRoute/pull/8307)).
diff --git a/changelog.d/fixes/9692-openai-to-claude-tool-images.md b/changelog.d/fixes/9692-openai-to-claude-tool-images.md
new file mode 100644
index 0000000000..c082d0dbc3
--- /dev/null
+++ b/changelog.d/fixes/9692-openai-to-claude-tool-images.md
@@ -0,0 +1 @@
+- **fix(translator):** convert OpenAI `image_url` blocks nested in `role: "tool"` / `tool_result` content to Claude `image` source blocks so OpenAI-compatible clients (Kimi Code CLI `ReadMediaFile`, and any other tool that returns media) no longer 400 the next Claude-format upstream turn ([#9692](https://github.com/diegosouzapw/OmniRoute/issues/9692))
diff --git a/changelog.d/fixes/9708-codex-same-account-retry.md b/changelog.d/fixes/9708-codex-same-account-retry.md
new file mode 100644
index 0000000000..2ccb7fb97f
--- /dev/null
+++ b/changelog.d/fixes/9708-codex-same-account-retry.md
@@ -0,0 +1 @@
+- **fix(resilience):** retry a retryable Codex pre-output 502/503/504/507 once on the same account (2–3s jitter) before cooling the connection, and stop translating that mixed pool into an all-accounts quota `429` ([#9708](https://github.com/diegosouzapw/OmniRoute/issues/9708))
diff --git a/changelog.d/fixes/PENDING-electron-window-hidden-hostname-bind.md b/changelog.d/fixes/PENDING-electron-window-hidden-hostname-bind.md
new file mode 100644
index 0000000000..6a88ba926a
--- /dev/null
+++ b/changelog.d/fixes/PENDING-electron-window-hidden-hostname-bind.md
@@ -0,0 +1 @@
+- **fix(electron):** desktop window stays hidden on Windows because the embedded Next.js server binds to the machine hostname instead of loopback ([#PENDING](https://github.com/diegosouzapw/OmniRoute/pull/PENDING))
diff --git a/changelog.d/fixes/assemble-standalone-cpsync-race.md b/changelog.d/fixes/assemble-standalone-cpsync-race.md
new file mode 100644
index 0000000000..82fabbcad6
--- /dev/null
+++ b/changelog.d/fixes/assemble-standalone-cpsync-race.md
@@ -0,0 +1 @@
+- fix(build): tolerate a same-realpath symlink or stale-typed dest in the standalone bundle assembler, fixing non-deterministic `ERR_FS_CP_EINVAL`/`ERR_FS_CP_DIR_TO_NON_DIR` crashes under heavy concurrent build I/O
diff --git a/changelog.d/fixes/claude-to-gemini-consecutive-roles.md b/changelog.d/fixes/claude-to-gemini-consecutive-roles.md
new file mode 100644
index 0000000000..17483dce52
--- /dev/null
+++ b/changelog.d/fixes/claude-to-gemini-consecutive-roles.md
@@ -0,0 +1 @@
+- **fix(translator):** merge consecutive same-role contents in direct Claude to Gemini request translation to prevent upstream HTTP 400 errors
diff --git a/changelog.d/fixes/combo-connection-scoped-reasoning-efforts.md b/changelog.d/fixes/combo-connection-scoped-reasoning-efforts.md
new file mode 100644
index 0000000000..3a53f1323a
--- /dev/null
+++ b/changelog.d/fixes/combo-connection-scoped-reasoning-efforts.md
@@ -0,0 +1 @@
+- **fix(catalog):** derive combo reasoning-effort tiers from the exact runtime-selectable connection scope, intersecting dynamic, pinned, allowlisted, and compatible provider-node evidence while failing closed on unknown capabilities.
diff --git a/changelog.d/fixes/minimax-music-generation-dispatch.md b/changelog.d/fixes/minimax-music-generation-dispatch.md
new file mode 100644
index 0000000000..e0cf2114ca
--- /dev/null
+++ b/changelog.d/fixes/minimax-music-generation-dispatch.md
@@ -0,0 +1 @@
+- **fix(sse):** MiniMax music models now generate audio instead of failing with `Unsupported music format: minimax-music` — the provider entry was registered in the music registry (and advertised by `/v1/models`), but `handleMusicGeneration` had no branch for its format, so every `minimax/*` music request fell through the dispatch chain to a 400. Adds the missing dispatch: a single synchronous POST with the `base_resp` envelope check (a non-zero `status_code` arrives on HTTP 200 too), `data.status` handling (an unfinished generation is reported instead of polled — the operation has no task id and no query endpoint), `url` and `hex` output formats (hex normalized to base64), `mp3`/`wav`/`pcm` containers via `audio_setting`, and the regional endpoint through the per-connection base-URL override, which is also the only host that accepts `aigc_watermark`. The registry entry gains the generation and cover model ids it was missing and drops a query URL that does not exist for this operation. Regression guard: `tests/unit/minimax-music-generation.test.ts` (9 tests).
diff --git a/changelog.d/maintenance/10317-latest-tracks-highest-stable.md b/changelog.d/maintenance/10317-latest-tracks-highest-stable.md
new file mode 100644
index 0000000000..9fdb4d2c81
--- /dev/null
+++ b/changelog.d/maintenance/10317-latest-tracks-highest-stable.md
@@ -0,0 +1 @@
+- **docs(docker):** spell out that `:latest` tracks the highest **published** stable SemVer (not git `main`), and that GitOps should pin `X.Y.Z` ([#10317](https://github.com/diegosouzapw/OmniRoute/issues/10317))
diff --git a/changelog.d/maintenance/10349-optional-work-event-loop.md b/changelog.d/maintenance/10349-optional-work-event-loop.md
new file mode 100644
index 0000000000..0cd695e4a7
--- /dev/null
+++ b/changelog.d/maintenance/10349-optional-work-event-loop.md
@@ -0,0 +1 @@
+- **docs(backend):** document that memory extraction, skills injection, and token refresh share the request event loop, plus dashboard kill switches ([#10349](https://github.com/diegosouzapw/OmniRoute/issues/10349))
diff --git a/changelog.d/maintenance/10350-sqlite-single-replica-ha.md b/changelog.d/maintenance/10350-sqlite-single-replica-ha.md
new file mode 100644
index 0000000000..9b158d5787
--- /dev/null
+++ b/changelog.d/maintenance/10350-sqlite-single-replica-ha.md
@@ -0,0 +1 @@
+- **docs(docker):** document default SQLite as single-replica / HA-unsupported, including Recreate and HEALTHCHECK session blast radius ([#10350](https://github.com/diegosouzapw/OmniRoute/issues/10350))
diff --git a/changelog.d/maintenance/10351-pre-write-backup-throttle.md b/changelog.d/maintenance/10351-pre-write-backup-throttle.md
new file mode 100644
index 0000000000..f6141d30ea
--- /dev/null
+++ b/changelog.d/maintenance/10351-pre-write-backup-throttle.md
@@ -0,0 +1 @@
+- **docs(backend):** document that pre-write SQLite backups (including models.dev pricing) are throttled to once per 60 minutes and can be disabled with `DISABLE_SQLITE_AUTO_BACKUP` ([#10351](https://github.com/diegosouzapw/OmniRoute/issues/10351))
diff --git a/changelog.d/maintenance/10775-remove-dead-enforce-secrets.md b/changelog.d/maintenance/10775-remove-dead-enforce-secrets.md
new file mode 100644
index 0000000000..4f44473360
--- /dev/null
+++ b/changelog.d/maintenance/10775-remove-dead-enforce-secrets.md
@@ -0,0 +1 @@
+- chore(security): remove the unused `enforceSecrets()` duplicate of the boot secret check and pin the live `enforceWebRuntimeEnv()` wiring with a regression test (#10775)
diff --git a/changelog.d/maintenance/10779-combo-invocation-docs.md b/changelog.d/maintenance/10779-combo-invocation-docs.md
new file mode 100644
index 0000000000..00138a3a65
--- /dev/null
+++ b/changelog.d/maintenance/10779-combo-invocation-docs.md
@@ -0,0 +1 @@
+- **docs:** Custom combos are only invoked by their exact name in the `model` field — `auto` remains a separate zero-config router, and `openrouter/auto` is a paid OpenRouter product, not an alias ([#10779](https://github.com/diegosouzapw/OmniRoute/pull/10779)) — thanks @maxmad64bis
diff --git a/changelog.d/maintenance/10780-server-init-dead-code.md b/changelog.d/maintenance/10780-server-init-dead-code.md
new file mode 100644
index 0000000000..ffe204a233
--- /dev/null
+++ b/changelog.d/maintenance/10780-server-init-dead-code.md
@@ -0,0 +1 @@
+- chore(startup): remove `src/server-init.ts` (183 lines, never imported — the boot path is `src/instrumentation-node.ts`) and correct four `"called from server-init.ts"` comments left pointing at the dead entry point (#10780)
diff --git a/changelog.d/maintenance/10875-combos-id-verb-coverage.md b/changelog.d/maintenance/10875-combos-id-verb-coverage.md
new file mode 100644
index 0000000000..5610a7ea41
--- /dev/null
+++ b/changelog.d/maintenance/10875-combos-id-verb-coverage.md
@@ -0,0 +1 @@
+- **docs(openapi):** document the `GET` and `PUT` operations on `/api/combos/{id}`, and add an operation-level coverage floor so a missing verb can no longer hide behind a path that already counts as covered ([#10875](https://github.com/diegosouzapw/OmniRoute/pull/10875))
diff --git a/changelog.d/maintenance/7786-management-auth-guide.md b/changelog.d/maintenance/7786-management-auth-guide.md
new file mode 100644
index 0000000000..54f84a79b9
--- /dev/null
+++ b/changelog.d/maintenance/7786-management-auth-guide.md
@@ -0,0 +1 @@
+- **docs(auth):** distinguish dashboard sessions, `oma_live_…` Access Tokens, manage-scoped API keys, and inference keys ([#7786](https://github.com/diegosouzapw/OmniRoute/issues/7786))
diff --git a/changelog.d/maintenance/env-doc-sync-adhoc-bot.md b/changelog.d/maintenance/env-doc-sync-adhoc-bot.md
new file mode 100644
index 0000000000..dbd759be29
--- /dev/null
+++ b/changelog.d/maintenance/env-doc-sync-adhoc-bot.md
@@ -0,0 +1 @@
+- **chore(ci):** ignore ad-hoc `BOT_TOKEN`/`BOT_URL` in env-doc-sync (scripts/ad-hoc mesh helpers, not runtime config)
diff --git a/changelog.d/maintenance/release-v3850-basereds-stream-utils-20260820.md b/changelog.d/maintenance/release-v3850-basereds-stream-utils-20260820.md
new file mode 100644
index 0000000000..98b62cd448
--- /dev/null
+++ b/changelog.d/maintenance/release-v3850-basereds-stream-utils-20260820.md
@@ -0,0 +1 @@
+- **fix(tests):** realign the two `stream-utils` passthrough cases that still asserted the pre-#10017 SSE framing — the event-boundary case declares the OpenAI Responses client format it actually exercises, and the metadata case now pins that surviving lines stay inside one event instead of expecting the `:`/`id:` control lines that #10473 stopped forwarding to every client format.
diff --git a/config/quality/dependency-allowlist.json b/config/quality/dependency-allowlist.json
index f276fc45d2..c4476f95d1 100644
--- a/config/quality/dependency-allowlist.json
+++ b/config/quality/dependency-allowlist.json
@@ -20,7 +20,6 @@
"@stryker-mutator/tap-runner",
"@swc/helpers",
"@tailwindcss/postcss",
- "@tensorflow/tfjs",
"@testing-library/jest-dom",
"@testing-library/react",
"@toon-format/toon",
diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json
index 2c4434e5a8..84bfc0e6ca 100644
--- a/config/quality/eslint-suppressions.json
+++ b/config/quality/eslint-suppressions.json
@@ -54,11 +54,6 @@
"count": 3
}
},
- "open-sse/handlers/chatCore/codexFailover.ts": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"open-sse/handlers/chatCore/comboContextCache.ts": {
"no-restricted-imports": {
"count": 1
@@ -1636,11 +1631,6 @@
"count": 1
}
},
- "src/lib/api/modelTestRunner.ts": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"src/lib/api/proxyRegistryRouteHandlers.ts": {
"no-restricted-imports": {
"count": 1
@@ -1696,11 +1686,6 @@
"count": 1
}
},
- "src/lib/embeddings/service.ts": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"src/lib/evals/runtime.ts": {
"no-restricted-imports": {
"count": 1
@@ -3609,11 +3594,6 @@
"count": 83
}
},
- "tests/unit/responses-parse-once-4041.test.ts": {
- "@typescript-eslint/no-explicit-any": {
- "count": 6
- }
- },
"tests/unit/responses-translation-fixes.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 35
diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json
index 05323c8002..72bc030875 100644
--- a/config/quality/file-size-baseline.json
+++ b/config/quality/file-size-baseline.json
@@ -1,4 +1,6 @@
{
+ "_rebaseline_2026_08_20_10531_freebuff_provider": "PR #10531 (adrianaryaputra, feat/freebuff-provider-support, closes #6793) own growth: src/shared/constants/providers/apikey/gateways.ts 1283->1298 (+15, the freebuff APIKEY_PROVIDERS_GATEWAYS catalog entry, additive data at the existing registry chokepoint, same god-file no-split rationale as prior gateways.ts rebaselines) and src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx 1062->1067 (+5, freebuff credential placeholder/hint at the existing per-provider switch chokepoint). Covered by tests/unit/freebuff-provider.test.ts (9/9 passing).",
+ "_rebaseline_2026_08_20_10574_reasoning_transport_fallback": "PR #10574 (jackjinke, fix/responses-reasoning-transport, fixes #10550) own growth: src/sse/handlers/chatHelpers.ts 1017->1019 (+2 = the new reasoningTransportFallback option threaded through executeChatWithBreaker's options destructure and its downstream handleSingleModel call, at the existing per-attempt options-passthrough chokepoint; not extractable without splitting the option-forwarding call itself). Covered by the PR's own reasoning-policy test suite (tests/unit/chatcore-translation-paths.test.ts, tests/unit/combo-attempt-body-isolation-7847.test.ts, tests/unit/reasoning-cache.test.ts, tests/unit/strip-reasoning-blobs-agentic-context-1599.test.ts among others), 446/446 focused tests passing.",
"_rebaseline_2026_08_18_10517_zed_hosted_oauth_callback_port": "PR #10517 (phatchau036, fix/zed-hosted-oauth-callback-port) own growth: src/shared/components/OAuthModal.tsx 1131->1148 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 1134->1149, +15/+18, crosses the frozen 1134 cap). Wires the zed-hosted native-app callback auto-complete: forceManual gating on isTrueLocalhost for zed-hosted, the loopback-redirect-URI comment block, and the exchangeToken full-URL-as-code branch, all at the existing provider-switch chokepoints this modal already carries growth for (seventh bump: 969->989->993->998->1030->1056->1100->1149; structural shrink tracked in #3501). The actual port-derivation logic lives in src/lib/oauth/providers/zed-hosted.ts (not frozen here) and was hardened during pre-merge review to use the server's own getRuntimePorts() instead of a browser-guessed scheme/port, covered by the new tests/unit/zed-hosted-loopback-port-derivation.test.ts (8/8 passing).",
"_rebaseline_2026_08_13_10243_codex_fingerprint_merge": "PR #10243 (xz-dev, Codex OAuth fingerprint convergence) merge into release/v3.8.50: src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts crossed the 1000-line new-file cap for the first time (974 on base, 997 on the PR's own branch, 1013 after merging + prettier reflow) purely from combining two independent, already-legitimate feature additions that landed on the same shared UI-helper file — this PR's own Codex fingerprint-mode select/toggle wiring (CODEX_FINGERPRINT_MODE_VALUES, getCodexFingerprintModeLabel, CodexFingerprintModeValue) plus #8949's unrelated Codex account-service-tier helpers merged concurrently on release/v3.8.50. Neither addition alone crosses the cap; git's line-level auto-merge does not detect a threshold crossing. Not modularized as part of this conflict-resolution merge commit (out of scope — this is a merge, not a feature change). Covered by the PR's own tests/unit/codex-fingerprint-convergence.test.ts, tests/unit/executor-codex.test.ts, tests/unit/provider-specific-data-schema.test.ts (all passing post-merge).",
"_rebaseline_2026_08_09_8984_api_key_cache_mode": "PR #8984 own growth during the 2026-08-09 rebase: src/lib/db/apiKeys.ts 1529->1545 (+16 = the per-key apiKeys.cacheDefaultMode column + its row parsers and cascade wiring; additive at the existing connection write/read chokepoints). Covered by tests/unit/chatcore-semantic-cache.test.ts. (chatCore.ts stays at the pre-existing base-red ceiling — upstream tip already exceeds the frozen 5042, this PR only adds +2 on top; not re-bumped per the no-inherit-ratchet rule.)",
@@ -304,6 +306,8 @@
"_rebaseline_2026_07_27_3850_relax_filesize_cap": "OWNER-APPROVED TEMPORARY relax for v3.8.50-3.8.54 PREPARE phase (docs/ROADMAP.md). cap 800->900 (+100), testCap 800->900 (+100). Targets: decompose-existing-frozen unchanged (frozen still only-shrink); this only relaxes the cap for NEW files in the decompose/extract-while-PREPARE phase (.51='executor registry in-place' and .52='combo.ts decomposition' create new leaf modules above 800). RE-TIGHTENING MANDATORY in v3.8.51: cap target 850 = 850 once decomposition wave stabilizes. SUPERSEDED by _rebaseline_2026_07_27_3850_relax_filesize_cap_v2_20pct (v1 +20% buffer) — retained for audit. Tracked via same roadmap issue.",
"_rebaseline_2026_07_27_v3849_train1h": "Merge-train 1H (31 PRs) — owner-approved 2026-07-27. Two distinct causes, kept separate on purpose: (1) GENUINE irreducible growth at existing chokepoints — providerLimits/auth (#8632 Kimi quota-reset recovery), rateLimitManager (#8616 idle wedged limiters), models-catalog-route.test (#8610 OpenCode Go effort aliases); (2) COLLISION with #8585, which banked shrinks measured on the pre-train release tip while 30 sibling PRs in the SAME train grew those files again — chat/accountFallback (#8628), chatCore (#8613), videoGeneration (#8581), imageGeneration. The zero-headroom frozen entries cannot absorb either. Ceilings re-pinned to the post-merge tip; #8612 (also in this train) automates shrink-banking so this self-inflicted drift stops recurring. Detail: src/lib/usage/providerLimits.ts 1006->1013 (#8632); src/sse/services/auth.ts 2492->2508 (#8632); open-sse/services/rateLimitManager.ts 1014->1060 (#8616); src/sse/handlers/chat.ts 1842->1845 (#8628); open-sse/handlers/chatCore.ts 4939->4955 (#8613); open-sse/handlers/imageGeneration.ts 3100->3101 ((sem PR — teto do #8585)); open-sse/handlers/videoGeneration.ts 1038->1063 (#8581); open-sse/services/accountFallback.ts 1965->1966 (#8628); tests/unit/models-catalog-route.test.ts 1608->1636 (#8610)",
"frozen": {
+ "_rebaseline_2026_08_20_10878_10799_provider_health_probes": "PRs #10878 (unsupported OpenAI-like validation probes stay neutral) + #10799 (preserve credential health on inconclusive NVIDIA-timeout/Antigravity-400 probes) own growth: src/app/api/providers/[id]/test/route.ts 946->1025 (+79, sum of both boarded together). Both add narrowly-scoped classification branches at the existing test-route dispatch chokepoint (unsupported-capability skip, credential-inconclusive detection) rather than new files, mirroring the prior 2026_06_27_5193 rebaseline of the same file. Covered by tests/unit/provider-validation-unsupported-neutral.test.ts + tests/unit/provider-health-inconclusive-probes.test.ts.",
+ "src/app/api/providers/[id]/test/route.ts": 1025,
"_rebaseline_2026_06_22_4644_deepseek_web_tools": "PR #4644 (BugsBag/robust deepseek-web tool-call parsing): open-sse/executors/deepseek-web.ts 1117->1125 (+8). The new agentic tool-call path emits surrounding text + reasoning before tool_calls and swaps to the dedicated deepseekWebTools.ts parser; the +8 lines are cohesive wiring at the existing transformSSE chokepoint (the parser itself lives in the new deepseekWebTools.ts file, already under cap). The PR's own fast-gate (PR->release) does not run check:file-size, so this surfaced only at release reconcile. Covered by tests/unit/deepseek-web-tools-variants.test.ts + deepseek-web-tools-execute.test.ts.",
"_rebaseline_2026_06_23_4712_deepseek_web_tool_results": "PR for #4712 (deepseek-web drops role:tool): open-sse/executors/deepseek-web.ts 1125->1148 (+23). messagesToPrompt() now folds role:\"tool\" results into the single-prompt transcript (recovering the tool name from the preceding assistant tool_calls by tool_call_id) instead of silently dropping them; the lines are cohesive wiring inside the existing function. Covered by tests/unit/deepseek-web-tool-result-prompt-4712.test.ts.",
"_rebaseline_2026_06_24_headroom_strategy": "Headroom-aware connection selection (dario technique): combo.ts 3168->3180 (+12 = a new `else if (strategy === \"headroom\")` dispatch branch in handleComboChat that delegates to orderTargetsByHeadroom + its log line, plus the import). The actual logic lives OUT of the god-file: the pure ranker rankByHeadroom/computeHeadroom is the new leaf open-sse/services/combo/headroomRanking.ts (91 LOC, 1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).",
"src/lib/modelCapabilities.ts": 1006,
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1014,
- "open-sse/config/imageRegistry.ts": 1019
+ "open-sse/config/imageRegistry.ts": 1034,
+ "src/sse/handlers/chatHelpers.ts": 1019,
+ "src/shared/middleware/chatBodyAdmission.ts": 1005,
+ "_rebaseline_2026_08_20_10668_tabitoken_gateway": "#10668 (yawar-aquil) own catalog growth: src/shared/constants/providers/apikey/gateways.ts 1268->1283 (+15, entirely this PR diff -- one new tabitoken gateway entry, data lines only; base moved from 1255 to 1268 via other merges since the PR forked). Not combination drift: reproducible on the PR branch alone, so the WS5.5 release-captain rule does not apply. Extraction is not available -- the file is pure data (own header: \"Pure data; merged by apikey/index.ts via spread\") and already split into 6 family files under apikey/. Same precedent as _rebaseline_2026_08_14_imagetotext_servicekinds (#10275/#10291, gateways.ts 1250->1255, data lines only) and _rebaseline_2026_08_11_v3850_merge_storm_provider_registry (owner-authorized for this same file)."
},
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
@@ -612,5 +619,8 @@
"_rebaseline_2026_08_12_proxyfetch_redaction": "Base-reds round 3 (#9985): proxyFetch.ts 1220->1239 (+19) = redactProxyDetailsInMessage() helper closing the credential leak #10032 reintroduced (raw proxy URL with user:password appended to the propagated error, Hard Rule #12); irreducible security fix at the existing error-surface chokepoint. Covered by tests/unit/tls-proxy-context.test.ts (strengthened leak guards).",
"_rebaseline_2026_08_12_modelcapabilities_snapshot_routing": "Base-reds round 3 (#9985): modelCapabilities.ts crossed the new-file cap at 1006 (+~10) when the context/max-input-token override lookups were routed through the #9199 bulk snapshot (fixing 323 per-model SQLite reads per catalog prepare — auto-combo-context-advertising guard); cohesive change at the existing resolution chokepoints, not extractable. Covered by tests/unit/auto-combo-context-advertising.test.ts + model-capability-resolution-snapshot-9199.test.ts.",
"_rebaseline_2026_08_14_imagetotext_servicekinds": "Image-to-Text category (#10275/#10291): gateways.ts grew 1250→1255 by data lines only — the serviceKinds: [\"llm\", \"imageToText\"] declarations on the openrouter and chutes catalog entries, plus the 3-line comment recording why chutes needs no static dots.ocr entry (passthroughModels discovery). No new logic or branching; the file is a provider catalog of declarative metadata. Splitting a catalog for five lines would be worse than the growth (semantic-families rule).",
- "_rebaseline_2026_08_18_imageregistry_merge_train": "merge-train 2026-08-18 (owner-authorized, /merge-prs batch of 84): open-sse/config/imageRegistry.ts crossed the 1000-line new-file cap for the first time purely from combining three independent, already-legitimate provider registrations boarded in the same local merge-train — #10542 (aihorde optional-key image catalog), #10494 (gemini-web image generation), #10594 (freepik/magnific provider rename + validation). 996 on release tip -> 1019 on the train tip. Each PR individually adds a small, additive IMAGE_PROVIDERS registry entry at the existing chokepoint; none crosses the cap alone. Not modularized as part of this train's gate fix (out of scope for a merge reconciliation, not a feature change). Covered by each PR's own focused tests (aihorde-image-catalog/generation, gemini-web image tests, freepik/magnific provider tests)."
-}
+ "_rebaseline_2026_08_18_imageregistry_merge_train": "merge-train 2026-08-18 (owner-authorized, /merge-prs batch of 84): open-sse/config/imageRegistry.ts crossed the 1000-line new-file cap for the first time purely from combining three independent, already-legitimate provider registrations boarded in the same local merge-train — #10542 (aihorde optional-key image catalog), #10494 (gemini-web image generation), #10594 (freepik/magnific provider rename + validation). 996 on release tip -> 1019 on the train tip. Each PR individually adds a small, additive IMAGE_PROVIDERS registry entry at the existing chokepoint; none crosses the cap alone. Not modularized as part of this train's gate fix (out of scope for a merge reconciliation, not a feature change). Covered by each PR's own focused tests (aihorde-image-catalog/generation, gemini-web image tests, freepik/magnific provider tests).",
+ "_rebaseline_2026_08_20_v3850_merge_train_batch1": "Merge-train batch1 (2026-08-19/20, 30 PRs boarded onto release/v3.8.50): gateways.ts 1255->1268 = PR #10722 (Token Kiosk OpenAI-compatible provider gateway catalog entry, +13 declarative lines, same god-file no-split rationale as prior gateways.ts rebaselines); chatHelpers.ts (uncapped, not previously frozen) new 1017 = PR #10797 (relay/bifrost error normalization, +23/-2, own-PR growth, existing file already near cap from accumulated chokepoint wiring per its own rebaseline history above); chatBodyAdmission.ts (uncapped) new 1005 = pre-existing base-red on the pure release tip (1004>1000 before this train boarded anything, no PR in this batch touches this file) — frozen here at its current size, not authorizing further growth. Owner-authorized rebaseline (2026-08-19 merge-prs session).",
+ "_rebaseline_2026_08_20_8338_cursor_image_provider": "PR (reimplementation of #8338, @valvesss): imageRegistry.ts 1019->1033 = new cursor IMAGE_PROVIDERS entry (Cursor plan image generation via Agent CLI), +14 lines of declarative provider metadata. Same god-registry no-split rationale as prior imageRegistry/gateways rebaselines.",
+ "_rebaseline_2026_08_20_imageregistry_1034": "imageRegistry.ts 1033->1034: +1 line drift between #10842 (cursor image provider, froze at 1033) and its actual merged state on release (measured 1034) — trivial rebaseline, not a new feature."
+}
\ No newline at end of file
diff --git a/config/quality/open-sse-typecheck-baseline.json b/config/quality/open-sse-typecheck-baseline.json
index 9b900ce2bd..c6de98b418 100644
--- a/config/quality/open-sse-typecheck-baseline.json
+++ b/config/quality/open-sse-typecheck-baseline.json
@@ -2,28 +2,10 @@
"open-sse/handlers/chatCore/clientUsageBuffer.ts": {
"TS2345": 2
},
- "open-sse/services/browserBackedChat.ts": {
- "TS2353": 2
- },
- "open-sse/services/compression/engines/omniglyphAdapter.ts": {
- "TS2307": 1
- },
- "open-sse/services/compression/stats.ts": {
- "TS2307": 1
- },
- "open-sse/utils/cursorImages.ts": {
- "TS2339": 1
- },
- "open-sse/utils/imageNormalize.ts": {
- "TS2339": 1
- },
"open-sse/utils/stream.ts": {
"TS2345": 2,
"TS2322": 2
},
- "open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/markdown.ts": {
- "TS2307": 2
- },
"src/lib/guardrails/videoBridgeHelpers.ts": {
"TS2488": 1,
"TS2365": 2,
diff --git a/config/quality/quality-baseline.json b/config/quality/quality-baseline.json
index 3c4b9e5191..e04be30d95 100644
--- a/config/quality/quality-baseline.json
+++ b/config/quality/quality-baseline.json
@@ -167,7 +167,8 @@
"dedicatedGate": true
},
"zizmorFindings": {
- "value": 190,
+ "value": 192,
+ "_rebaseline_2026_08_20_radar_export_workflow": "190 -> 192 (+2). Workflow novo `.github/workflows/radar-export.yml` (passo 10 do go-live do Radar: publica o export estável do catálogo como asset de release para o servidor privado baixar via RADAR_EXPORT_URL). Os +2 são unpinned-uses @vN: actions/checkout@v7 + actions/setup-node@v7 — a MESMA convenção deliberada de todos os workflows (ver _scanner_harden_workflows_2026_06_16); fixar por SHA só este violaria a convenção. O findings artipacked do checkout foi CORRIGIDO com `persist-credentials: false` (o job publica via GH_TOKEN em `gh release`, não usa a credencial do checkout). Nenhuma classe nova de template-injection / cache-poisoning / dangerous-triggers. Medido local com zizmor 1.25.2 via `node scripts/check/check-workflows.mjs --ratchet` = 191; +1 do delta conhecido do runner (ver _rebaseline_2026_07_28_ci_runner_delta: o runner enxerga 1 unpinned-uses @vN a mais que o devbox no mesmo commit; a baseline segue o runner) => 192.",
"_rebaseline_2026_07_20_aliasresolver_hook_split_7808": "175 -> 176 (+1). Companion to PR #7808 (CodeQL js/incomplete-url-substring-sanitization fix in bin/aliasResolver.mjs). The +1 is NOT caused by this PR's code changes (bin/* is not a workflow file) — it is a pre-existing drift that surfaced because the ratchet gate runs on this PR's CI: the zizmor scanner version on the GitHub runner gained a new rule (or extended an existing one) since the v3.8.49 baseline was seeded on 2026-07-17. Breakdown: the new finding is an unpinned-uses @vN class item on one of the existing workflows (same deliberate convention as _scanner_harden_workflows_2026_06_16 — @vN is intentional, SHA-pinning only this one would violate the convention). No new template-injection/artipacked/cache-poisoning/dangerous-triggers classes introduced. Measured by the Quality Gates (Extended) job on run 29713001401 = 176, baseline was 175. Note: by the time this landed on release/v3.8.49, the baseline was already at 176 via _rebaseline_2026_07_17_combo_recovery_hints — this entry is kept as historical record; no further bump applied.",
"_rebaseline_2026_07_17_v3849_release": "169 -> 175 (+6). Cycle workflow drift (v3.8.48/v3.8.49): npm-publish.yml (new, WS1.3 #7092), electron-release.yml, nightly-compat.yml, nightly-release-green.yml, CI restructures (#7501 full-history base fetch, #7355 main-green, #7202 merge-queue gates, Trunk/Codecov). Breakdown vs v3.8.47: +3 unpinned-uses (@vN convention, deliberate per _scanner_harden_workflows_2026_06_16), +2 cache-poisoning (artifact upload/cache in the OWN electron-release/npm-publish RELEASE workflows -- operator-controlled, not fork-PR exploitable), +1 excessive-permissions (nightly-compat.yml permissions:issues). No new template-injection/artipacked/dangerous-triggers. Measured with zizmor 1.25.2 via `node scripts/check/check-workflows.mjs --ratchet` = 175 on da3a0be69.",
"direction": "down",
diff --git a/docs/architecture/ADAPTIVE_ROUTING.md b/docs/architecture/ADAPTIVE_ROUTING.md
new file mode 100644
index 0000000000..73bacfec3a
--- /dev/null
+++ b/docs/architecture/ADAPTIVE_ROUTING.md
@@ -0,0 +1,350 @@
+---
+title: "Adaptive Routing: Routing Events, Quality Feedback & Explainability"
+version: 3.8.50
+lastUpdated: 2026-08-20
+---
+
+# Adaptive Routing: Routing Events, Quality Feedback & Explainability
+
+This document describes the feedback-driven adaptive routing foundation added to
+OmniRoute. It is deliberately small: it introduces a typed routing-outcome
+channel, an online quality signal that feeds the existing auto-combo scorer, an
+optional OpenTelemetry exporter, and an explainability endpoint. It does **not**
+replace the existing resilience stack (circuit breaker, connection cooldown,
+model lockout, health matrix, autopilot) — it complements it.
+
+## 1. Architectural context
+
+OmniRoute is a data plane with a **request hot path** and a **control/intelligence
+plane**. The hot path must stay fast, memory-efficient, asynchronous, resilient and
+predictable. Evaluation, quality scoring, experiments and historical analysis belong
+to the control plane.
+
+```
+AI Agent / IDE
+ │
+ ▼
+┌─────────────────────┐
+│ OmniRoute │ data plane (fast, sync, in-memory)
+│ routing / failover │
+│ health / guardrail │
+│ cache / streaming │
+└──────────┬──────────┘
+ │ RoutingEvent (fire-and-forget, ~0.2µs)
+ ▼
+┌─────────────────────┐
+│ Feedback sinks │ control plane (async, best-effort)
+│ quality tracker │
+│ OTel exporter │
+│ explain store │
+└──────────┬──────────┘
+ ▼ quality score
+ auto-combo scorer
+```
+
+### What was already there (audited, not duplicated)
+
+| Concept | Existing implementation |
+| ----------------------------------- | -------------------------------------------------------------------------------------------------- |
+| Availability (can we send traffic?) | Circuit breaker (CLOSED/DEGRADED/OPEN/HALF_OPEN, DB-persisted), connection cooldown, model lockout |
+| Health reporting | `providerHealthMatrix.ts`, `providerHealthAutopilot.ts` |
+| Shadow traffic | `open-sse/services/combo/shadowRouting.ts` |
+| Guardrails | `src/lib/guardrails/` (pre/post hooks) |
+| Exact cache | `src/lib/semanticCache.ts` (signature-based) |
+| Evaluators / eval-driven routing | `src/lib/evals/`, `open-sse/services/evalRouting.ts` |
+| Combo decision explainability | `open-sse/services/combo/decisionTrace.ts` |
+| Dashboard real-time events | `src/lib/events/eventBus.ts` (UI notification channel, `unknown` payloads, 100-entry history) |
+
+The routing-event layer is **not** a re-implementation of `eventBus`: that bus is
+the dashboard's real-time notification channel (typed _event names_, opaque
+payloads, UI consumers). `RoutingEvent` is a typed _outcome_ struct
+(latency/tokens/cost/outcome/finish-reason) consumed by the control plane's
+feedback sinks (quality tracker, OTel exporter, explain store).
+
+### What was missing (added here)
+
+1. A **typed routing-outcome event + sink abstraction** (`RoutingEvent` /
+ `RoutingEventSink`). `decisionTrace` is combo-scoped and in-memory-only;
+ `comboMetrics` are cumulative counters; `call_logs` is raw async persistence.
+ None is a typed, sink-based outcome channel that a quality tracker, an OTel
+ exporter, or a Future-AGI-style evaluator can subscribe to.
+2. An **online quality signal** (EWMA) for output quality — the scorer previously
+ proxied "quality" only through static task fitness and opt-in eval pass-rates.
+3. An **optional, dependency-free OTel exporter** using GenAI semantic conventions.
+4. An **explainability endpoint** returning the real routing decisions + quality state.
+
+## 2. Routing Events (feedback foundation)
+
+Files: `open-sse/services/routing/events.ts`, `.../index.ts`
+
+A `RoutingEvent` carries only routing metadata:
+
+```ts
+interface RoutingEvent {
+ requestId: string;
+ provider: string;
+ model: string;
+ strategy: string; // "auto" | "priority" | "direct" | ...
+ latencyMs: number;
+ ttftMs: number | null;
+ inputTokens: number | null;
+ outputTokens: number | null;
+ cost: number | null;
+ retries: number;
+ fallbackUsed: boolean;
+ outcome: RoutingOutcome; // allowlisted union
+ status: number | null;
+ finishReason: string | null;
+ connectionId: string | null;
+ ts: number;
+}
+```
+
+`RoutingEventSink` is a `Send+Sync`-style trait in TypeScript:
+
+```ts
+interface RoutingEventSink {
+ readonly name: string;
+ record(event: RoutingEvent): void; // must be O(1), no sync I/O
+}
+```
+
+The hot path calls `emitRoutingEvent(event)` once per completed request
+(the streaming-completion callback, the non-streaming success path, and the
+malformed-200 failure path in `handleChatCore`). Dispatch is synchronous fan-out
+to registered sinks, but each sink only enqueues/updates in-memory state. **No
+synchronous database writes, no network I/O on the hot path.**
+
+Default sinks:
+
+- `MemoryRoutingEventStore` — bounded (500) ring buffer, newest-first, for the
+ explain endpoint.
+- `QualityTracker` consumer — updates the EWMA quality estimate.
+- `OtlpHttpsEventSink` — optional, enabled only when `OMNIROUTE_OTEL_ENDPOINT`
+ (or `OTEL_EXPORTER_OTLP_ENDPOINT`) is set.
+
+### Measured overhead (honest comparison)
+
+`npm run bench:routing-events` on this workstation (100k iterations; sub-µs ops
+measured as aggregate µs/op because per-op percentiles are below
+`performance.now()` timer resolution):
+
+| Scenario | µs/op | ops/s |
+| --------------------------------- | ------ | ------ |
+| baseline (scoring only) | ~0.045 | ~22 M |
+| baseline + RoutingEvent (2 sinks) | ~0.168 | ~5.9 M |
+| baseline + event + OTel enqueue | ~0.163 | ~6.1 M |
+| concurrent (8 interleaved bursts) | ~0.18 | — |
+
+The event-dispatch delta over baseline scoring is ~0.12 µs/request; the OTel sink
+only enqueues (O(1) buffer push), adding nothing measurable. These numbers are
+machine-specific and relative — not a production guarantee. The v1 "~0.2 µs"
+figure was an aggregate estimate; this methodology separates the scoring baseline
+from the event-dispatch cost.
+
+## 3. Quality Signal (feedback-driven provider state)
+
+Files: `open-sse/services/routing/quality.ts`
+
+v2 separates **operational** from **semantic** quality:
+
+- **Operational** — derived from the routing hot path (HTTP 4xx/5xx, connection
+ failures, 429s, malformed responses, stream interruptions, `finish_reason=length`,
+ zero-output successes, latency/TTFT EWMA). A 200 is NOT treated as semantic
+ quality.
+- **Semantic** — the actual value of the generated output. ONLY ever produced by
+ an evaluator via `setSemanticQuality()`. It is `null` until one provides it and
+ never leaks into the operational score.
+
+Per-(provider, model) state (EWMA + bounded counters):
+
+- `successEwma` — EWMA (α=0.2) of outcome success.
+- `latencyEwma` / `ttftEwma` — EWMA of latency (α=0.1).
+- `samples`, `anomalies`, `rateLimited`, `semantic`, `semanticConfidence`.
+- `recencyMs` — how recently the model was last observed.
+
+### Confidence / sample awareness
+
+`confidence = clamp01(samples / 50)`, and the score returned to the scorer is
+blended toward the neutral midpoint:
+
+```
+score = 0.5 + confidence * (operational - 0.5)
+```
+
+Consequences (verified by tests):
+
+- A cold provider (0 samples) scores **0.5** — not unfairly penalized, but
+ unable to dominate a provider with thousands of solid observations.
+- A provider with 7 lucky successes is pulled toward 0.5 (never dominates from
+ optimistic initialization).
+- A provider with 50+ samples converges to its true operational score.
+- Degradation and recovery are gradual (EWMA), and one isolated failure does
+ not destroy a healthy provider.
+
+`ProviderQuality` exposes `{ operational, semantic, confidence, samples, anomalies,
+rateLimited, successEwma, latencyEwmaMs, ttftEwmaMs, recencyMs }`.
+
+This feeds the auto-combo scorer as the `quality` scoring factor:
+
+- `ScoringFactors.quality` / `ScoringWeights.quality` in
+ `open-sse/services/autoCombo/scoring.ts`.
+- `DEFAULT_WEIGHTS`: `health` 0.1905 → 0.1605, `quality` 0.03. Sum stays 1.0.
+- `buildAutoCandidates` populates `candidate.quality` from the tracker; candidates
+ without data default to neutral **0.5** (a cold candidate is neither boosted nor
+ penalized).
+
+The closed loop:
+
+```
+RoutingEvent → QualityTracker → getQualityScore → auto-combo quality factor
+ ↑ │
+ └────── request outcome (handleChatCore) ←────────────┘
+```
+
+### Hard exclusion vs soft penalty
+
+The quality signal is a **soft adaptive preference** only. Hard exclusion stays
+with the existing resilience stack: circuit breaker OPEN, quota exhausted,
+auth failure, model lockout — none of these are affected by the quality score.
+A provider whose quality score dips temporarily is de-preferenced, never
+hard-disabled.
+
+## 3b. Canonical stream timing (TTFT / ITL)
+
+Files: `open-sse/utils/streamTiming.ts`
+
+`createStreamTiming()` is the single instrumentation seam for the streaming path,
+wired into `createSSEStream` (open-sse/utils/stream.ts):
+
+- `markByte()` — first upstream chunk received.
+- `markForward()` — first chunk forwarded to the client (used for TTFT).
+- `markInterrupted()` — stream timeout/abort/error before a clean finish.
+- `ttft()` = first-forwarded-SSE-chunk latency. **This is NOT token-level TTFT** —
+ a single SSE chunk may carry zero/one/many tokens. Documented precisely.
+- `avgItlMs()` = mean inter-chunk gap (a chunk-latency proxy for ITL).
+
+TTFT/ITL/interrupted flow into the `RoutingEvent` (`ttftMs`, `itlMs`) and are
+exported as GenAI/OmniRoute span attributes by the OTel sink.
+
+## 4. OpenTelemetry / GenAI observability
+
+Files: `open-sse/services/routing/otel.ts`
+
+- Dependency-free OTLP/HTTP JSON exporter (uses global `fetch`, no
+ `@opentelemetry/*` SDK).
+- Spans follow GenAI semantic conventions (`gen_ai.provider.name`,
+ `gen_ai.request.model`, `gen_ai.usage.input_tokens/output_tokens`,
+ `gen_ai.completion.finish_reason`, `gen_ai.system`) plus OmniRoute routing
+ attributes (outcome, status, ttft, retries, fallback).
+- `record()` only enqueues into a bounded buffer (O(1)); a background timer
+ flushes via `POST {endpoint}/v1/traces` asynchronously. Under overload the
+ oldest events are dropped (`dropped` counter) — never backpressure the data
+ plane.
+- **Disabled unless configured.** `OMNIROUTE_OTEL_ENDPOINT` (or
+ `OTEL_EXPORTER_OTLP_ENDPOINT`) must be set; otherwise the sink is not
+ registered and zero OTel code runs.
+
+## 5. Explainability
+
+- `GET /v1/explain/routing` returns the recent `RoutingEvent`s (the real
+ decisions, newest first) and the per-provider/model quality snapshot.
+- Auth mirrors `/v1/combos` (Bearer API key or dashboard session; anonymous on
+ single-user local deployments with `REQUIRE_API_KEY=false`).
+- Combo-level per-invocation traces remain available via the existing
+ `decisionTrace.ts` (header `X-OmniRoute-Combo-Trace`).
+- Safety: events carry only routing metadata, never prompts/bodies/credentials.
+
+## 6. Evaluation-plane integration (Future AGI readiness)
+
+OmniRoute treats Future AGI (or any evaluator) as a **potential
+intelligence/evaluation backend, not a dependency**. The seams:
+
+- A `RoutingEventSink` can forward events to an evaluator asynchronously.
+- The `MemoryRoutingEventStore` + quality snapshot give an evaluator the raw
+ decision stream.
+- A future `Evaluator` (deterministic, local judge, HTTP, WASM) would consume
+ events/traces and return a `QualityScore` that feeds the same
+ `getQualityScore`/quality-factor path.
+- Existing eval-driven routing (`open-sse/services/evalRouting.ts`) already
+ re-orders combo targets by `eval_runs` pass-rates when enabled.
+
+No evaluation runs synchronously on the request path, and the gateway operates
+fully with the evaluator absent.
+
+## 7. Final architectural review
+
+1. **What remains on the synchronous hot path?** Routing/scoring, guardrail
+ pre-checks, cache lookup, and one `emitRoutingEvent` fan-out (~0.12 µs over
+ baseline scoring) to in-memory sinks.
+2. **What moved to asynchronous processing?** OTel export (timer + fetch),
+ `call_logs`/usage persistence, semantic-cache writes, quality is in-memory
+ and O(1) (no async needed).
+3. **How does a routing outcome become feedback?** `handleChatCore` emits a
+ `RoutingEvent` → `QualityTracker` updates EWMA state → `getQualityScore`
+ feeds the auto-combo `quality` factor.
+4. **How does quality influence future routing?** A low quality score reduces
+ the weighted score of that provider/model in `scoreAutoTargets`, so degraded
+ models are gradually de-preferenced and recover as their EWMA improves.
+5. **How can Future AGI integrate without becoming a dependency?** Via the
+ `RoutingEventSink` interface / a future `Evaluator` adapter — no hardcoded
+ dependency.
+6. **What happens when the evaluator is unavailable?** Routing is unaffected;
+ quality falls back to neutral (1.0) for models with no observed signal.
+7. **What happens when telemetry is unavailable?** The OTel sink simply isn't
+ registered; the rest of the routing layer runs unchanged.
+8. **What happens under overload?** The OTel buffer drops oldest events; quality
+ and the ring buffer are bounded by construction; no backpressure.
+9. **How does provider state recover after degradation?** EWMA re-converges as
+ successes accumulate; warmup keeps cold models neutral; the circuit breaker
+ independently recovers via HALF_OPEN probes.
+10. **Which proposed features were intentionally NOT implemented, and why?**
+ - Shadow traffic / experiments — already implemented
+ (`combo/shadowRouting.ts`); not re-built.
+ - Guardrails — already implemented (`src/lib/guardrails/`); not duplicated.
+ - Semantic cache — already implemented (`src/lib/semanticCache.ts`); not
+ duplicated.
+ - A full experiment-management platform, dataset tooling, prompt-optimization
+ platform, vector DB, or mandatory external OTel infrastructure — out of
+ scope for a lean data plane.
+ - A Rust `RoutingEvent` struct — the data plane is TypeScript; the TS type
+ is the adapted equivalent.
+
+## 8. Configuration reference
+
+| Variable | Default | Effect |
+| ----------------------------- | ----------- | ------------------------------------------------------------------------------- |
+| `OMNIROUTE_OTEL_ENDPOINT` | unset | When set, enables the OTLP/HTTP traces exporter (e.g. `http://collector:4318`). |
+| `OTEL_EXPORTER_OTLP_ENDPOINT` | unset | Fallback alias for the OTLP endpoint. |
+| `OTEL_SERVICE_NAME` | `omniroute` | `service.name` resource attribute. |
+
+## 9. Tests
+
+- `tests/unit/routing-events.test.ts` — event normalization, status
+ classification, bounded ring buffer, sink fan-out + isolation.
+- `tests/unit/routing-quality.test.ts` — EWMA warmup, failure/success recovery,
+ anomaly penalties, 429 transient handling, snapshot, reset.
+- `tests/unit/routing-scoring-quality.test.ts` — weight integrity, neutral
+ default, quality factor ranking.
+- `tests/unit/routing-otel.test.ts` — enable gating, GenAI span payload, async
+ flush, drop-under-overload.
+- `tests/unit/routing-events-concurrency.test.ts` — thousands of events, ring
+ buffer boundedness, throwing-sink isolation, interleaved async bursts,
+ reset-during-inserts.
+- `tests/unit/routing-adaptive-e2e.test.ts` — deterministic end-to-end loop via
+ the real `scoreAutoTargets` scorer: healthy → degrade → recover → blip, plus
+ cold-start and lucky-cold-provider scenarios.
+- `tests/unit/stream-timing.test.ts` — TTFT (first-forwarded-chunk), ITL,
+ first-byte vs first-forward, interruption, malformed/empty chunk safety.
+
+## 10. Pre-existing issues status (Phase 18)
+
+| Issue | Status | Notes |
+| ----------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `omniglyph` export mismatch | **FIXED (environmental)** | `node_modules` was out of sync with `package-lock.json` (installed 1.3.1 vs locked 1.4.0). Running `npm install omniglyph@1.4.0` restored the locked version; type errors dropped to 0. Manifests unchanged. |
+| Stale `getKnownContextOverflow` tests | **KNOWN — not fixed** | `combo-context-overflow-compression-probe.test.ts` imports a function that no longer exists in `open-sse/services/combo.ts` (only comments reference it). Fixing requires re-implementing or re-writing those tests — unrelated architectural churn. |
+| `combo-runtime-unit-concurrency.test.ts` DB isolation | **KNOWN — not fixed** | Test-harness SQLite-isolation assertion fails when run directly; fails identically on the base branch. |
+| i18n `llm.txt` drift | **KNOWN — not fixed** | `docs/i18n/*/llm.txt` differ from root; pre-existing, blocks the docs-sync pre-commit gate. |
+
+Environmental vs code issues are kept distinct; no unrelated failures are hidden
+behind changed test filters.
diff --git a/docs/architecture/CODEBASE_DOCUMENTATION.md b/docs/architecture/CODEBASE_DOCUMENTATION.md
index dacf0d6f58..23099237db 100644
--- a/docs/architecture/CODEBASE_DOCUMENTATION.md
+++ b/docs/architecture/CODEBASE_DOCUMENTATION.md
@@ -89,7 +89,6 @@ src/
├── i18n/ Locale bundles
├── instrumentation.ts Next.js instrumentation hook
├── instrumentation-node.ts
-├── server-init.ts Process-level bootstrap (env, DB, jobs, sync)
└── proxy.ts Top-level proxy bootstrap helper
```
diff --git a/docs/architecture/REPOSITORY_MAP.md b/docs/architecture/REPOSITORY_MAP.md
index f61131d594..4d59418054 100644
--- a/docs/architecture/REPOSITORY_MAP.md
+++ b/docs/architecture/REPOSITORY_MAP.md
@@ -133,7 +133,6 @@ src/
├── types/ # Shared TS type files
├── instrumentation.ts # Next.js telemetry hook (browser + edge)
├── instrumentation-node.ts # Node-only instrumentation
-├── server-init.ts # Server bootstrap (DB migrations, jobs, cleanup)
└── proxy.ts # HTTP-proxy entry shim
```
diff --git a/docs/architecture/meta.json b/docs/architecture/meta.json
index dba5872324..c5b10b7923 100644
--- a/docs/architecture/meta.json
+++ b/docs/architecture/meta.json
@@ -12,6 +12,7 @@
"ROUTER_BACKENDS",
"admission-lanes",
"cluster-decisions",
- "persistence-backend-boundary"
+ "persistence-backend-boundary",
+ "ADAPTIVE_ROUTING"
]
}
diff --git a/docs/compression/COMPRESSION_ENGINES.md b/docs/compression/COMPRESSION_ENGINES.md
index 22cffe7c42..4a232a0e5d 100644
--- a/docs/compression/COMPRESSION_ENGINES.md
+++ b/docs/compression/COMPRESSION_ENGINES.md
@@ -28,12 +28,12 @@ The `omniglyph` engine (package `omniglyph`, 1.4.0+) accepts a named semantic pr
globally through `omniglyph.profile` in the compression settings or per step through the
stacked pipeline's step config:
-| Profile | Boundary |
-| -------------- | --------------------------------------------------------------------------- |
-| `aggressive` | Default. The policy the published receipts measured — images system, tool docs and dense history |
-| `balanced` | Keeps live state native, protects the last 8 turns, collapses older closed history |
-| `coding-safe` | Keeps authority, tool schemas and live tool output native, protects the last 12 turns |
-| `passthrough` | Routes without transforming; the engine is skipped |
+| Profile | Boundary |
+| ------------- | ------------------------------------------------------------------------------------------------ |
+| `aggressive` | Default. The policy the published receipts measured — images system, tool docs and dense history |
+| `balanced` | Keeps live state native, protects the last 8 turns, collapses older closed history |
+| `coding-safe` | Keeps authority, tool schemas and live tool output native, protects the last 12 turns |
+| `passthrough` | Routes without transforming; the engine is skipped |
The profile is a **ceiling, not a floor**: `mergeCompressionProfileOptions` in the package
refuses to let a caller override reopen a lossy lane the profile closed, so a per-step
@@ -170,22 +170,22 @@ override points it at a local copy instead (offline / air-gapped installs).
### Optional dependencies & on-demand install
-The prunable LLMLingua runtime peer stack is **optional**. Three packages are declared as
+The prunable LLMLingua runtime peer stack is **optional**. Two packages are declared as
`optionalDependencies` in `package.json` and kept **external** by the production build
(`scripts/build/prepublish.ts` does not bundle them):
-| Package | Version (pin) | Notes |
-| -------------------- | ------------- | ---------------------------------------------- |
-| `@atjsh/llmlingua-2` | `2.0.3` | Entry package; declares the others as peers |
-| `@tensorflow/tfjs` | `4.22.0` | Heaviest dep — dominates the ~800 MB footprint |
-| `js-tiktoken` | `^1.0.20` | Tokenizer |
+| Package | Version (pin) | Notes |
+| -------------------- | ------------- | ------------------------------------------- |
+| `@atjsh/llmlingua-2` | `2.0.5` | Entry package; declares the others as peers |
+| `js-tiktoken` | `^1.0.20` | Tokenizer |
-`@huggingface/transformers` is pinned at `3.5.2` as an **optional** dependency (shared with
-the local embeddings path and also traced into the standalone bundle). Keeping it optional prevents
-`onnxruntime-node` CUDA provider postinstall failures on CUDA 11 hosts from aborting the whole
-OmniRoute install; when the optional stack is absent, LLMLingua still fail-opens. Only the three
-packages above are prunable SLM peers. A standard `npm install` (dev) installs the optional stack
-automatically unless optional dependencies are omitted.
+`@huggingface/transformers` is pinned at `^4.2.0` (shared with the local embeddings path and
+also traced into the standalone bundle); `@atjsh/llmlingua-2@2.0.5` peers on it with
+`"^3.5.2 || ^4.0.0"`, so both Transformers.js v3 and v4 are supported. Since 2.0.4,
+`@atjsh/llmlingua-2` no longer requires `@tensorflow/tfjs`, which removed the largest single
+contributor (TensorFlow.js) from the SLM stack. Only the two packages above are prunable SLM
+peers. A standard `npm install` (dev) installs the optional stack automatically unless optional
+dependencies are omitted.
**Why on-demand:** the npm-published package, the standalone bundle, and the Docker image
ship **without** these deps to stay slim. When they are absent, the worker's dependency
@@ -195,11 +195,12 @@ error logged). To activate it in a pruned environment, install the optional stac
```bash
# pin to the versions declared in package.json optionalDependencies
-npm install @atjsh/llmlingua-2@2.0.3 @tensorflow/tfjs@4.22.0 js-tiktoken
+npm install @atjsh/llmlingua-2@2.0.5 js-tiktoken
```
-Roughly **~800 MB** total: the TensorFlow.js + transformers runtimes dominate; the
-TinyBERT model adds ~57 MB downloaded at first use (not via npm).
+The `@tensorflow/tfjs` removal (2.0.4+) eliminates the previously dominant ~800 MB
+contributor — the remaining footprint is the transformers.js + onnxruntime-node runtimes,
+plus the TinyBERT model (~57 MB) downloaded at first use (not via npm).
Per environment:
diff --git a/docs/diagrams/cli-terminal.svg b/docs/diagrams/cli-terminal.svg
index 99bc29b327..4fd6887859 100644
--- a/docs/diagrams/cli-terminal.svg
+++ b/docs/diagrams/cli-terminal.svg
@@ -1,6 +1,6 @@
-