v3.8.40

diegosouzapw/OmniRoutev3.8.40Jun 29, 2026by diegosouzapw

AI Summary

Introduces advanced compression features like relevance extraction and hard-budget mode.

Key Highlights

  • Compression: Relevance extractive engine, hard-budget mode, result memoization, and saliency heatmap.
  • Adds `omniroute_tool_search` MCP tool and RTK semantic renderers.
  • New provider: Microsoft 365 Copilot individual provider.
  • Fixes Docker image promotion and command-code max_tokens handling.
  • Improves OAuth refresh guards and tray icon functionality.

New Features

  • Compression relevance engine, hard-budget mode, memoization, transparency annotation, saliency heatmap, composite-command splitter, MCP tool search, RTK semantic renderers, QuantumLock, M365 Copilot individual provider, Docker tag promotion fix, command-code max_tokens fix, OAuth refresh CAS guard, import cycle fix, compression analytics fix, tray icon fix, Antigravity envelope fix, GitHub releases fallback, combo session stickiness, Kiro filler fix, combo 400 handling, disabled no-auth providers, dashboard context parent, i18n fix, SSE accept header fix, deprecated models removal, local_shell removal, Antigravity retry, grok-cli params, grok-cli import object fix, qoder coalesce, proxy fallback cache fix, MCP HTTP auth fix, logging headers fix, textual tag extraction fix.

Full Release Notes

## [3.8.40] — TBD

_In development — bullets added per PR; finalized at release._

### ✨ New Features

- **feat(compression): relevance extractive engine** — a new opt-in compression engine that scores each sentence by term-overlap (Jaccard) with the user's last query minus a length/boilerplate penalty, greedily keeps the most relevant within a budget, and reconstructs the original order. Pure-string, deterministic, ReDoS-safe (char-code tokenization, no `RegExp` over user input), fail-open, default off. Ideal for trimming long pasted RAG context / tool output to what's relevant. Sentences carrying real signal (digits/URLs/errors/code/paths) are never dropped; `overlapThreshold`/`budgetPercent`/`boilerplateWeight` are configurable. Tier-2 item of the compression feature-extraction roadmap (#7). ([#5289](https://github.com/diegosouzapw/OmniRoute/pull/5289))
- **feat(compression): hard-budget mode — compress to ≤ N tokens** — a deterministic post-pass (`targetTokens` / `targetRatio`, default unset → no-op) that trims a body to a token budget. It ranks sentences/lines by average `scoreToken` ascending and drops the lowest-saliency ones until the body fits (measured by the exact cl100k `countTextTokens`), preserving original order. Lines carrying real signal (digits, URLs, `Error:`-family, code fences, stack `at`-frames, multi-segment paths, `key=value`) are never dropped; the budget is distributed proportionally across messages so the total stays ≤ target; an unreachable target (all-preserved) surfaces a `validationWarnings` note instead of failing silently. Does NOT touch the `estimateCompressionTokens` budget-gate estimator. Tier-3 item of the compression feature-extraction roadmap (#17). ([#5288](https://github.com/diegosouzapw/OmniRoute/pull/5288), follow-up [#5291](https://github.com/diegosouzapw/OmniRoute/pull/5291))
- **feat(compression): result memoization for deterministic engines (opt-in)** — caches `(input, config) → result` for provably pure, stateless modes (`lite`/`standard`/`rtk` and stacked pipelines of `{lite,caveman,rtk}`) to skip recompute on the hot path. Opt-in via `memoizeCompressionResults` (default off → zero behavior change). Conservative opt-in whitelist (stateful `ccr`/`session-dedup` — which write the cross-request CCR store — and model-backed `ultra`/`aggressive`/`llmlingua` are never cached), principal-scoped (skipped without a principal, so no cross-principal body leak), and clone-on-store + clone-on-read. Tier-3 item of the compression feature-extraction roadmap (#21). ([#5286](https://github.com/diegosouzapw/OmniRoute/pull/5286))
- **feat(compression): inline transparency annotation** — surfaces `tokens=847→312; rules: filler×8, dedup×2` derived from existing compression stats. The `X-OmniRoute-Compression` response header is extended **append-only** (the `mode; source=X` prefix stays byte-identical, so existing header parsers don't break) and the compression studio cockpit shows a matching badge. Zero new computation — it aggregates the `rulesApplied`/`techniquesUsed` already on the stats. Tier-3 item of the compression feature-extraction roadmap (#18). ([#5284](https://github.com/diegosouzapw/OmniRoute/pull/5284))
- **feat(compression): saliency heatmap in the compression studio** — the preview studio can now color each token by saliency: `ultra` per-token `scoreToken` (0–1, green→red gradient) or universal kept/removed from the existing diff. A dry-run visualization behind a toggle (no cost on a normal preview; backward-compatible when off). Completes the visualization half of roadmap item #13 (the A/B comparison shipped in [#5080](https://github.com/diegosouzapw/OmniRoute/pull/5080)). ([#5285](https://github.com/diegosouzapw/OmniRoute/pull/5285))
- **feat(compression): composite-command splitter for RTK detection** — `cd /x && git status` now detects as `git-status` (previously the whole string was treated as one command and matched no filter). A quote-aware top-level tokenizer splits on `&&`/`||`/`;` (never inside quotes or `$(…)`/backtick subshells) and feeds the **last** segment to RTK command detection, so every RTK filter/renderer fires on commands wrapped in `cd … &&`/`||`/`;` chains. O(n), no RegExp over the command (ReDoS-safe). Tier-3 item of the compression feature-extraction roadmap (#16). ([#5283](https://github.com/diegosouzapw/OmniRoute/pull/5283))
- **feat(mcp): `omniroute_tool_search` tool + one-line TS signatures** — new MCP tool that does lexical keyword search over every MCP tool's name/description and returns the top matches as compact one-line TypeScript signatures (~half the JSON-schema token cost), so agents discover tools on demand instead of carrying all ~88 schemas every turn. Search is ReDoS-safe (substring scoring, never `new RegExp` on the query) and deterministic; `tools/list` stays complete (no hidden tools). Adds the `read:tools` scope. Tier-1 item of the compression feature-extraction roadmap. ([#5269](https://github.com/diegosouzapw/OmniRoute/pull/5269))
- **feat(compression): RTK semantic command-output renderers (opt-in)** — adds a second, opt-in compaction layer to the RTK engine that rewrites structured command output into a far more compact semantic form: `git diff` → file headers + `@@` hunks + changed lines only; an all-green `pytest`/`jest`/`vitest`/`eslint` run → its one-line summary; `terraform`/`tofu plan` → `Plan: +N ~M -K` plus the resource list; `kubectl`/`aws` JSON arrays → a minimal table. Each renderer is conservative (no-op when the shape doesn't match) and the integration is fail-open; the test-green renderer never collapses output that carries any failure signal. Gated by `RtkConfig.enableRenderers` (default off → zero behavioral change). Eighth item of the compression feature-extraction roadmap. ([#5268](https://github.com/diegosouzapw/OmniRoute/pull/5268))
- **feat(compression): QuantumLock cache-prefix stabilization (opt-in, default off)** — recovers upstream prompt-cache hits that a volatile fragment in the system prompt would otherwise bust. When a caller injects a session UUID, unix timestamp, request-id, JWT, API-key shape, or long hex digest into the `role:system` message every turn, the longest common prefix across turns ends at that changing byte → the whole system prompt after it is re-billed and re-processed each turn. QuantumLock replaces each non-semantic volatile fragment with a **positional, value-independent** placeholder `⟦Q{i}⟧` and appends the real values in a delimited `⟦QUANTUMLOCK⟧` tail. The rewrite is **sent to the model** (lossless — not restored), so the system-prompt body becomes **byte-identical across turns** and the provider caches the long stable prefix while only the small tail differs. Opt-in, default off, applied only for caching providers (`isCachingProvider && config.quantumLock.enabled`); bounded ReDoS-safe patterns; idempotent; **no date/time patterns** (semantically meaningful — explicit non-goal). Studio gets a toggle + a "🔒 N volatile fragment(s) stabilized" dry-run badge. Seventh item of the compression feature-extraction roadmap (bench: [#5080](https://github.com/diegosouzapw/OmniRoute/pull/5080), gate: [#5127](https://github.com/diegosouzapw/OmniRoute/pull/5127), fuzzy: [#5143](https://github.com/diegosouzapw/OmniRoute/pull/5143), ionizer: [#5148](https://github.com/diegosouzapw/OmniRoute/pull/5148), TOON: [#5163](https://github.com/diegosouzapw/OmniRoute/pull/5163), CCR ranged: [#5187](https://github.com/diegosouzapw/OmniRoute/pull/5187), risk-gate: [#5243](https://github.com/diegosouzapw/OmniRoute/pull/5243)). ([#5260](https://github.com/diegosouzapw/OmniRoute/pull/5260))
- **kilocode:** anonymous (no-auth) access to Kilo Code's free models, mirroring the `opencode`/`mimocode` pattern. With no Kilo account connected, requests now fall back to the gateway's anonymous tier (`Authorization: Bearer anonymous` on `api.kilo.ai/api/openrouter`) so the free models work without signup; a connected OAuth account is still used unchanged for the paid tier ([#5259](https://github.com/diegosouzapw/OmniRoute/pull/5259), #4019 — thanks @Theadd for the reference implementation)
- **feat(logging): call-log correlation ID (end-to-end)** — every request now gets a unique correlation id, returned in the `X-Correlation-Id` response header, persisted in `call_logs` (migration **109**), filterable via `/api/usage/call-logs`, and surfaced in the dashboard request logger (per-chunk stream timestamps + active-requests-first sort). This is the safe, cohesive core subset of the larger #5275 — landed on its own so the low-risk value isn't blocked by the parts of that PR still under review. ([#5279](https://github.com/diegosouzapw/OmniRoute/pull/5279) — thanks @hartmark)
- **feat(providers): Microsoft 365 Copilot individual provider** — adds the `copilot-m365-web` provider (the 237th), wiring the M365 BizChat framing/connection helpers into a selectable web-session provider backed by `m365.cloud.microsoft/chat` for individual Microsoft 365 plans. Builds on the M365 pure-framing groundwork from #4696. Regression guard: `tests/unit/copilot-m365-web-executor.test.ts`. ([#5302](https://github.com/diegosouzapw/OmniRoute/pull/5302) — thanks @skyzea1)

### 🔧 Bug Fixes

- **ci(docker):** re-point the Docker Hub / GHCR `:latest` (and `:latest-web`) tags to the just-published release. On a `release: released` event the freshly-created git tag is often not yet visible to `git fetch --tags` when `docker-publish` runs, so the `:latest`-promotion gate built its candidate set purely from `git tag -l` and resolved the highest semver to the **previous** version — leaving `latest` one release behind (3.8.39 published, `latest` still 3.8.38). The decision now lives in `scripts/ci/should-promote-latest.sh`, which folds the current `VERSION` into the candidate set before picking the highest stable semver, making promotion independent of tag-sync timing (a patch published after a higher minor still won't grab `latest`). Regression guard: `tests/unit/build/should-promote-latest-5301.test.ts` ([#5301](https://github.com/diegosouzapw/OmniRoute/issues/5301))
- **command-code:** treat a non-positive `max_tokens`/`max_completion_tokens` (e.g. Zoo Code's `-1` "let the server choose") as "no limit" — omit the field instead of forcing it to `1`. `clampMaxTokens` previously did `Math.max(1, …)`, so a client `-1` was sent upstream as `max_tokens: 1`, truncating the response to a single token (the observed `completion_tokens: 1`, `content: null`, `reasoning_content: "The"` with `finish_reason: stop`). Now any value `≤ 0` is dropped so Command Code applies the model's native default; positive values are still floored and clamped to the 200k ceiling. Regression guard: `tests/unit/command-code-maxtokens-negative-5166.test.ts` ([#5166](https://github.com/diegosouzapw/OmniRoute/issues/5166) — thanks @Stazyu)
- **fix(auth): compare-and-swap guard on the OAuth refresh persist** — under multi-agent load, the per-connection refresh mutex makes `[network refresh + DB write]` atomic for **one** connection, but it does not protect against a **third** writer (a sibling request, a concurrent HealthCheck, or a replica) landing a fresher `refresh_token` rotation on the same `connection_id` between the staleness read and the persist. Overwriting that fresher row reverts the sibling's rotation; the next caller then loads the now-consumed token, Auth0/Anthropic flag it as `refresh_token_reused`, and the whole token family gets revoked (the 1352× claude/`aa5dd5cf` invalidation storm). `getAccessToken` now re-reads the row's current `refresh_token` immediately before persisting (inside the mutex) and **skips the write** when it has rotated past the token the caller presented — the caller still receives the freshly-issued access token, only the DB overwrite is skipped. Opt-in via `runWithCasGuard` (no active guard ⇒ byte-identical behavior); skip/persist counters exposed via `getCasGuardStats()`. Regression guard: `tests/unit/token-refresh-cas-guard-4038.test.ts`. ([#4038](https://github.com/diegosouzapw/OmniRoute/issues/4038) — thanks @KooshaPari for the root-cause diagnosis)
- **mcp:** break the `schemas/tools.ts ↔ schemas/toolSearch.ts` import cycle introduced when the `tool_search` defs (#5269) were extracted into their own module — `toolSearch.ts` imported `McpToolDefinition` from `tools.ts` while `tools.ts` imported `toolSearchTool` from `toolSearch.ts`, failing `check:cycles` on `release/v3.8.40`. The shared `AuditLevel` + `McpToolDefinition` types now live in a leaf `schemas/toolDefinition.ts` that both import; `tools.ts` re-exports them for backward compatibility.
- **compression (analytics):** record attempted-but-no-op compression runs so Stacked is no longer invisible when it saves nothing. Previously a `compression_analytics` row was written only on a net-positive saving, so a Stacked (RTK→Caveman) pipeline that ran on already-compact context produced no row — indistinguishable from "never dispatched" (`byMode.stacked.count` stayed flat while Ultra climbed). Such runs are now recorded with `skip_reason` and surfaced as a per-mode `skipped` count plus `totalSkipped`/`bySkipReason` in the analytics summary and the Mode Breakdown; the existing net-saving totals/averages are unchanged (skip rows are excluded from them) (#4268 — thanks @abdulkadirozyurt, @androw)
- **cli (tray):** fix `omniroute server --tray` showing no tray on macOS/Linux with no error printed. The wired Unix tray path loaded `systray2` through an inline loader that called `require("module")` inside an ESM `.mjs` file (`"type":"module"`) → `ReferenceError: require is not defined`, silently swallowed (regressed in v3.8.34); even if it had loaded, `systray2` isn't in `node_modules` (it's lazily installed into `~/.omniroute/runtime`). The loader now delegates to the runtime loader, the icon path (`icon.png`) is corrected, `isTemplateIcon` is `false` (the full-color icon rendered as a white square under macOS template mode), and tray start failures are surfaced to stderr instead of being swallowed (#4605 — thanks @ProgMEM-CC)
- **agent-bridge (antigravity):** unwrap the cloudcode-pa `.request` envelope when converting Antigravity IDE requests. The real IDE sends `cloudcode-pa.googleapis.com/v1internal:generateContent` with the Gemini request nested under `.request` (`{ project, model, request: { contents, systemInstruction, generationConfig } }`), but the bridge read those fields at the top level — yielding an empty conversation, so prompts hung mid-execution. The legacy `/v1beta/models/<model>:generateContent` top-level shape still works (#4294 — thanks @shabeer)
- **dashboard:** add a GitHub releases fallback to the "Update Available" lookup. After the v3.8.28 fix added an npm-registry HTTP fallback, the banner could still stay hidden on networks that reach GitHub (where the news feed already loads) but not `registry.npmjs.org`. `resolveLatestVersion()` now tries npm CLI → npm registry → GitHub releases (`/repos/diegosouzapw/OmniRoute/releases/latest`) before giving up, and logs a warning only when all three fail (#4100)
- **command-code:** omit `max_tokens` when the client omits it so the upstream applies the model's native default, fixing `400 "expected <=200000"` on `/alpha/generate` for high-cap models; an explicit oversized client value is clamped to the 200k endpoint ceiling (#5221 — thanks @adivekar-utexas)
- **combo:** wire session stickiness into the round-robin dispatch path. Multi-turn conversations from clients that send no session id (Codex CLI, Claude Code, most OpenAI-compatible tools) were rotated to a different connection on every turn by round-robin combos, busting the upstream prompt-cache → cold high-reasoning starts, intermittent `504`s and throughput collapse under concurrency. The weighted/priority paths already honored per-conversation stickiness; the round-robin handler returned before reaching it. Round-robin now starts the rotation at the conversation's sticky connection (failover to the other targets is preserved), and different conversations still spread across connections — only intra-conversation rotation is removed ([#5248](https://github.com/diegosouzapw/OmniRoute/pull/5248), #3825 — thanks @bypanghu, @jpsn123, @xz-dev)
- **kiro:** replace the synthesized trailing `"Continue"` turn with a neutral filler (`"..."`) — when an OpenAI→Kiro request ends on an assistant/tool turn, the translator synthesizes the protocol-required trailing user turn, and the literal word `"Continue"` could be read by Kiro/CodeWhisperer as a real user instruction and trigger unintended agent action. A trailing tool-result turn is still promoted as-is (it already collapses to a real user turn); only the assistant-text-ending case is affected. Regression guards: `tests/unit/kiro-continue-filler-5231.test.ts`. ([#5231](https://github.com/diegosouzapw/OmniRoute/issues/5231))
- **combo:** advance to the next combo target on a `400 "requested model is not supported"` instead of hard-failing. The 400 guard in the priority strategy treated `MODEL_CAPACITY` as a block-fallback reason, so a combo that hit a provider lacking a specific model returned a hard `400` even when other targets (different providers) supported it. Such 400s now fall through to the next target. ([#5249](https://github.com/diegosouzapw/OmniRoute/pull/5249) — thanks @Chewji9875)
- **dashboard:** disabled no-auth providers no longer vanish from the All Providers page. Disabling a no-auth provider (the "No authentication required" toggle, which adds it to `blockedProviders`) silently removed its card because the page _dropped_ blocked no-auth entries from its render list — the only way back was buried under Settings → Security → Blocked Providers. The page now **partitions** no-auth entries: visible providers render as before, blocked ones appear in a "Disabled" sub-group with an **Enable** button that un-blocks them in place. Aggregates, counts and `/v1/models` still consume the visible-only list (blocked providers stay out of routing). Regression guard: `tests/unit/noauth-blocked-partition-5183.test.ts`. ([#5183](https://github.com/diegosouzapw/OmniRoute/issues/5183), follow-up from [#5166](https://github.com/diegosouzapw/OmniRoute/issues/5166) — thanks @WslzGmzs)
- **dashboard:** add a parent `/dashboard/context` page so RSC prefetches of the compression-context hub no longer 404. The route only had sub-routes (`settings`, `combos`, `ultra`, …) and no parent page, so the App Router returned 404 for the bare segment. The parent now redirects to its canonical sub-route (`/dashboard/context/settings`), honoring a legacy `?tab=` query for deep links. Regression guard: `tests/unit/dashboard/context-parent-redirect-5298.test.ts` ([#5298](https://github.com/diegosouzapw/OmniRoute/issues/5298) — thanks @KooshaPari)
- **i18n:** add the missing `sidebar.gamificationGroup` message across all 42 locales — the Gamification sidebar group referenced a `titleKey` that existed in no locale, logging `MISSING_MESSAGE: sidebar.gamificationGroup (en)` at runtime (the group still rendered via its `titleFallback`). The key is now present everywhere so the warning is gone and locale coverage is unaffected ([#5298](https://github.com/diegosouzapw/OmniRoute/issues/5298) — thanks @KooshaPari)
- **api(stream):** `/v1/chat/completions` no longer returns SSE for a non-stream OpenAI-compatible request when `stream` is omitted and the client sends `Accept: application/json, text/event-stream` — the Vercel AI SDK / OpenAI SDK non-stream signature (`doGenerate()`/`generateText()`), which then failed with `Invalid JSON response` (`Unexpected token 'd', "data: {"id"...`). The route-level Accept override (#302) and `resolveStreamFlag` now treat an Accept header that explicitly lists `application/json` as a JSON opt-in even when it also lists `text/event-stream`; only a pure `Accept: text/event-stream` (no `application/json`) still opts an omitted-`stream` request into SSE, and an explicit body `stream` value always wins. The shared decision now lives in `acceptHeaderForcesStream`. Regression guard: `tests/unit/sse-nonstream-accept-5305.test.ts`. ([#5305](https://github.com/diegosouzapw/OmniRoute/issues/5305) — thanks @md-riaz)
- **providers:** drop the retired GPT‑5.2 / GPT‑4.5 models from the direct **ChatGPT‑web** and **Codex** surfaces (OpenAI removed them there), so OmniRoute stops advertising/routing models that no longer exist. Scoped on purpose to those two providers — third‑party proxies that still expose the ids are untouched. ([#5280](https://github.com/diegosouzapw/OmniRoute/pull/5280) — thanks @backryun)
- **codex:** drop the deprecated `local_shell` hosted tool type before forwarding to OpenAI's Responses API, resolving the omni-combo `400 "The local_shell tool is no longer supported."` spike. Inbound Responses `local_shell` is still accepted and mapped to a caller-side Chat `shell` function for compatibility. ([#5250](https://github.com/diegosouzapw/OmniRoute/pull/5250), [#5256](https://github.com/diegosouzapw/OmniRoute/pull/5256) — thanks @KooshaPari)
- **antigravity:** retry excluded accounts via the fallback LRU. The combo same-model retry loop accumulates excluded Antigravity connection ids after account-level failures, but auth selection only treated a single `excludeConnectionId` as a fallback scenario — once exclusions accumulated through `excludedConnectionIds`, selection could fall back to normal sticky/priority behavior instead of LRU-selecting the next eligible account for the same model/family. Any non-empty accumulated exclude set is now treated as fallback mode. Builds on the family-scoped lockout work in #5180 (v3.8.39). ([#5222](https://github.com/diegosouzapw/OmniRoute/pull/5222) — thanks @Ardem2025)
- **grok-cli:** strip unsupported sampling params (`presencePenalty`, `frequencyPenalty`, `logprobs`, `topLogprobs`) before sending to the Grok Build API, fixing `400 'Model does not support parameter presencePenalty'` when clients (MiMoCode, Cursor, etc.) send OpenAI-style params. ([#5273](https://github.com/diegosouzapw/OmniRoute/pull/5273) — thanks @fulorgnas)
- **grok-cli:** accept the full `~/.grok/auth.json` object in the dashboard import-token endpoint. The `oauthImportTokenSchema` only accepted a bare string token while the UI sends the whole auth.json object → `400 Bad Request`; the schema now accepts the object and stores the original under `providerSpecificData.rawAuthJson` for diagnostics and token refresh. ([#5258](https://github.com/diegosouzapw/OmniRoute/pull/5258) — thanks @fulorgnas)
- **qoder:** coalesce concurrent PAT→job-token exchanges per PAT so high-concurrency / multi-agent bursts no longer stampede `openapi.qoder.sh/api/v1/jobToken/exchange` before the first exchange populates the completed-token cache; the shared exchange is also decoupled from any single caller's `AbortSignal` so one aborted waiter can't cancel it for the others. ([#5254](https://github.com/diegosouzapw/OmniRoute/pull/5254), [#5265](https://github.com/diegosouzapw/OmniRoute/pull/5265) — thanks @KooshaPari)
- **proxy:** scope the fallback reachability cache by normalized target **URL** instead of by hostname, so a failed probe for one endpoint on a shared API host no longer suppresses a later probe for a different endpoint on that same host for the full TTL (host fallback is preserved for malformed URLs). ([#5261](https://github.com/diegosouzapw/OmniRoute/pull/5261) — thanks @KooshaPari)
- **proxy:** cache failed fast-fail health probes with a short negative TTL instead of the full positive health TTL, so a single transient timeout/load blip no longer marks a working residential SOCKS5 proxy unreachable for the whole window (#5109 regression coverage added). ([#5255](https://github.com/diegosouzapw/OmniRoute/pull/5255) — thanks @KooshaPari)
- **mcp:** forward HTTP auth to internal tool fetches so MCP tools that call back into the local API surface carry the caller's authorization. ([#5218](https://github.com/diegosouzapw/OmniRoute/pull/5218) — thanks @KooshaPari)
- **logging:** preserve the **outbound provider request** headers in the detailed call-log Provider Request payload (previously the upstream **response** headers were shown there). chatCore now keeps executor-returned request headers when wrapping streaming and non-streaming responses; response headers stay scoped to the `Response`. ([#5257](https://github.com/diegosouzapw/OmniRoute/pull/5257) — thanks @rdself)
- **sse:** scope textual `<think>`/`<thinking>` tag extraction so generic OpenAI-compatible paths don't rewrite prompt-format content into `reasoning_content`; an explicit opt-in keeps tag-native families (DeepSeek-R1, QwQ) working while Antigravity/Agy stay excluded by provider/model prefix. ([#5224](https://github.com/diegosouzapw/OmniRoute/pull/5224) — thanks @rdself)
- **mcp:** break the `schemas/tools.ts ↔ schemas/toolSearch.ts` import cycle introduced when the `tool_search` defs (#5269) were extracted into their own module — `toolSearch.ts` imported `McpToolDefinition` from `tools.ts` while `tools.ts` imported `toolSearchTool` from `toolSearch.ts`, failing `check:cycles` on `release/v3.8.40`. The shared `AuditLevel` + `McpToolDefinition` types now live in a leaf `schemas/toolDefinition.ts` that both import; `tools.ts` re-exports them for backward compatibility. ([#5282](https://github.com/diegosouzapw/OmniRoute/pull/5282))
- **compression (analytics):** record attempted-but-no-op compression runs so Stacked is no longer invisible when it saves nothing. A `compression_analytics` row was previously written only on a net-positive saving, so a Stacked pipeline that ran on already-compact context produced no row — indistinguishable from "never dispatched". Such runs are now recorded with `skip_reason` and surfaced as a per-mode `skipped` count plus `totalSkipped`/`bySkipReason`; net-saving totals/averages are unchanged (skip rows excluded). ([#5277](https://github.com/diegosouzapw/OmniRoute/pull/5277), #4268 — thanks @abdulkadirozyurt, @androw)
- **cli (tray):** fix `omniroute server --tray` showing no tray on macOS/Linux with no error printed. The Unix tray path loaded `systray2` through an inline loader that called `require("module")` inside an ESM `.mjs` file → `ReferenceError: require is not defined`, silently swallowed (regressed in v3.8.34); even if loaded, `systray2` is lazily installed into `~/.omniroute/runtime`, not `node_modules`. The loader now delegates to the runtime loader, the icon path is corrected, `isTemplateIcon` is `false` (the full-color icon rendered as a white square under macOS template mode), and tray start failures surface to stderr. ([#5276](https://github.com/diegosouzapw/OmniRoute/pull/5276), #4605 — thanks @ProgMEM-CC)
- **agent-bridge (antigravity):** unwrap the cloudcode-pa `.request` envelope when converting Antigravity IDE requests. The real IDE sends `cloudcode-pa.googleapis.com/v1internal:generateContent` with the Gemini request nested under `.request`, but the bridge read those fields at the top level — yielding an empty conversation, so prompts hung mid-execution. The legacy `/v1beta/models/<model>:generateContent` top-level shape still works. ([#5267](https://github.com/diegosouzapw/OmniRoute/pull/5267), #4294 — thanks @shabeer)
- **dashboard:** add a GitHub releases fallback to the "Update Available" lookup. After the v3.8.28 npm-registry fallback, the banner could still stay hidden on networks that reach GitHub but not `registry.npmjs.org`. `resolveLatestVersion()` now tries npm CLI → npm registry → GitHub releases before giving up. ([#5266](https://github.com/diegosouzapw/OmniRoute/pull/5266), #4100)
- **command-code:** omit `max_tokens` when the client omits it so the upstream applies the model's native default, fixing `400 "expected <=200000"` on `/alpha/generate` for high-cap models; an explicit oversized client value is clamped to the 200k endpoint ceiling. ([#5221](https://github.com/diegosouzapw/OmniRoute/pull/5221) — thanks @adivekar-utexas)

### 🔒 Security

- **authz:** require auth for the `/v1beta/*` Gemini-compatible client API. `next.config.mjs` rewrote `/v1beta/:path*` → `/api/v1beta/:path*`, but `src/proxy.ts` didn't match `/v1beta` before the rewrite and `classifyRoute()` didn't classify `/api/v1beta/*` as client API — so unauthenticated `/v1beta/models/...:generateContent` traffic could reach the model-serving route without the central client-API auth policy. Both alias and rewritten forms are now classified `CLIENT_API`, enforcing Bearer auth when `REQUIRE_API_KEY` is enabled. ([#5274](https://github.com/diegosouzapw/OmniRoute/pull/5274) — thanks @rdself)
- **sentinel:** security hardening pass across request handling. ([#5241](https://github.com/diegosouzapw/OmniRoute/pull/5241) — thanks @iamedwardngo)
- **providers:** refresh impersonation User-Agents + TLS fingerprint profiles to current real-client versions; several had drifted or were inconsistent across files, a bot-detection/blocking risk. ([#5237](https://github.com/diegosouzapw/OmniRoute/pull/5237) — thanks @backryun)
- **authz (public origin):** centralize browser-mutation origin validation into `src/server/origin/publicOrigin.ts` and wire it through the authz pipeline, replacing the per-route same-origin-only check that `403`'d dashboard mutations when served behind a reverse proxy on a different public origin. The module resolves the allowed public origin from configured base-URL env vars or trusted forwarded headers (only when `OMNIROUTE_TRUST_PROXY` is set **and** the peer is loopback/LAN via peer-stamp), validates `Sec-Fetch-Site` metadata, and sanitizes `Host`/`Forwarded` inputs (rejects control chars, userinfo, path/query in Host). Regression guards: `tests/unit/authz/public-origin.test.ts` + `tests/unit/authz/pipeline.test.ts`. ([#5278](https://github.com/diegosouzapw/OmniRoute/pull/5278) — thanks @Thinkscape / @abodera)

### 📝 Maintenance

- **docs:** reorganize `docs/`, run an accuracy audit, and drop Node 20 from the supported matrix (rebased onto the current release tip). ([#5262](https://github.com/diegosouzapw/OmniRoute/pull/5262))
- **providers:** remove the discontinued Gemini CLI channel — Google shut it down on 2026-06-18; the supported migration path is Antigravity. ([#5246](https://github.com/diegosouzapw/OmniRoute/pull/5246) — thanks @rdself)
- **dashboard:** add more provider icons from Lobehub. ([#5220](https://github.com/diegosouzapw/OmniRoute/pull/5220) — thanks @backryun)
- **deps:** resolve `npm install` warnings, fix a runtime `Cannot find module` crash on `omniroute serve` (the runtime-env module was missing from the npm `files` allow-list), and clear the 4 moderate `npm audit` advisories. ([#5252](https://github.com/diegosouzapw/OmniRoute/pull/5252) — thanks @yunaamelia), ([#5230](https://github.com/diegosouzapw/OmniRoute/pull/5230), #5227)
- **docker:** harden the base image against container-scan CVEs and skip comment lines in the #4076 builder heap-ordering check. ([#5229](https://github.com/diegosouzapw/OmniRoute/pull/5229), [#5233](https://github.com/diegosouzapw/OmniRoute/pull/5233))
- **ci:** Trivy advisory scan now ignores unfixed CVEs to cut Security-tab noise. ([#5235](https://github.com/diegosouzapw/OmniRoute/pull/5235))
- **test(combo):** reconcile the #4279 stop-guard test with the #5249 advance policy. ([#5300](https://github.com/diegosouzapw/OmniRoute/pull/5300))
- **test(targets):** add 14 unit tests for the shared `combo/targetExhaustion.ts` handler (provider-exhausted / connection-error / transient rate-limited classification), registered in the vitest discovery list. ([#5296](https://github.com/diegosouzapw/OmniRoute/pull/5296) — thanks @KooshaPari)

---



## What's Changed
* chore(docker): harden base image against container-scan CVEs by @diegosouzapw in https://github.com/diegosouzapw/OmniRoute/pull/5228
* chore(ci): Trivy advisory scan ignores unfixed CVEs (Security-tab noise) by @diegosouzapw in https://github.com/diegosouzapw/OmniRoute/pull/5234
* fix: centralize public origin checks for proxied dashboards by @Thinkscape in https://github.com/diegosouzapw/OmniRoute/pull/5278
* Release v3.8.40 by @diegosouzapw in https://github.com/diegosouzapw/OmniRoute/pull/5225


**Full Changelog**: https://github.com/diegosouzapw/OmniRoute/compare/v3.8.39...v3.8.40