diff --git a/.env.example b/.env.example index 77c4a23dea..1f292cf0de 100644 --- a/.env.example +++ b/.env.example @@ -1786,6 +1786,23 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis # Alias for BIFROST_API_KEY (used by scripts that read the env via # OMNIROUTE_*). Falls back to BIFROST_API_KEY when unset. # OMNIROUTE_BIFROST_KEY= +# Relay backend selection for the OpenAI-compatible relay endpoint: +# ts | bifrost | auto. "ts" (default when Bifrost is not configured) uses the +# TypeScript relay; "auto" selects Bifrost when BIFROST_BASE_URL is set (and +# BIFROST_ENABLED != 0) and falls back to TS if the sidecar is unreachable; +# "bifrost" forces Bifrost (strict — no TS fallback). Auth, rate limits, +# injection guard and model allowlists always run in the Next route first. +# RELAY_ROUTING_BACKEND is an accepted alias. Responses carry X-Routing-Backend +# and X-Routing-Fallback. +# OMNIROUTE_RELAY_BACKEND= +# RELAY_ROUTING_BACKEND= +# Opt-in native HTTPS/TLS for `omniroute serve` (equivalent to --tls-cert / +# --tls-key). Provide BOTH a PEM certificate and its private key and the +# standalone server terminates TLS on the same listener (wss:// works +# unchanged). With neither set the server stays plain HTTP; providing only one +# (or an unreadable path) logs a warning and stays HTTP (never half-enables). +# OMNIROUTE_TLS_CERT= +# OMNIROUTE_TLS_KEY= # ─── 1-click local service launchers (PR-3 in #3932) ──────────────────────── # Master switch for /api/local/* routes. When unset or "0", all /api/local/* diff --git a/CHANGELOG.md b/CHANGELOG.md index f6a9ea7442..550e93ad4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,39 @@ --- -## [3.8.40] — TBD +## [3.8.41] — 2026-06-29 -_In development — bullets added per PR; finalized at release._ +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + +## [3.8.40] — 2026-06-29 ### ✨ New Features diff --git a/README.md b/README.md index ae9c08b8aa..7d82821ca9 100644 --- a/README.md +++ b/README.md @@ -290,7 +290,7 @@ Result: 4 layers of fallback = zero downtime -> Recent highlights from **v3.8.20 → v3.8.39**. Full history in [`CHANGELOG.md`](CHANGELOG.md). +> Recent highlights from **v3.8.20 → v3.8.41**. Full history in [`CHANGELOG.md`](CHANGELOG.md). - **⚖️ Quota-Share routing** — a dedicated combo strategy that spreads load across accounts by _available quota_: Deficit-Round-Robin scheduling, per-connection `max_concurrent` with cooldown-wait queueing, multi-window usage buckets (5h / 7d / per-model), per-(key,model) caps, session stickiness for prompt-cache integrity, and proactive saturation from upstream token-usage headers. → [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md) - **🤖 One-command CLI/agent setup** — a dedicated `setup-*` command configures each coding tool to route through OmniRoute (Claude Code, Codex, Cline, Continue, Cursor, Roo Code, Kilo Code, Crush, Goose, Qwen Code, Aider, OpenCode); `omniroute launch` / `omniroute launch-codex` are zero-config launchers. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md) @@ -302,7 +302,7 @@ Result: 4 layers of fallback = zero downtime - **🧠 Memory you control** — opt-in int8 vector quantization (Qdrant + sqlite-vec), memory off by default, and a per-request `x-omniroute-no-memory` header. → [Memory](docs/frameworks/MEMORY.md) - **🛡️ Security** — a prompt-injection guard across every LLM route (backed by a red-team suite), plus a free DuckDuckGo last-resort web search. → [Guardrails](docs/security/GUARDRAILS.md) - **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 237-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech / transcription / music / video), and one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`). → [Providers](docs/reference/PROVIDER_REFERENCE.md) -- **⚡ Local performance & infra** — a one-click local Redis launcher (`omniroute redis up`, plus a dashboard Redis panel), one-click **Cloudflare Workers** and **Deno Deploy** relay deployers wired into the proxy pool, and an optional Bifrost Go sidecar that offloads the hottest relay path (`BIFROST_BASE_URL`, with automatic fallback to the TypeScript path on timeout). → [Environment](docs/reference/ENVIRONMENT.md) +- **⚡ Local performance & infra** — a one-click local Redis launcher (`omniroute redis up`, plus a dashboard Redis panel), one-click **Cloudflare Workers** and **Deno Deploy** relay deployers wired into the proxy pool, and an optional Bifrost Go sidecar that offloads the hottest relay path (`BIFROST_BASE_URL`, with automatic fallback to the TypeScript path on timeout) — now with a relay-backend selector (`OMNIROUTE_RELAY_BACKEND=ts|bifrost|auto`) so the `/v1/relay` endpoint stays the stable surface while choosing the fastest backend internally. → [Environment](docs/reference/ENVIRONMENT.md)
diff --git a/bin/cli/commands/serve.mjs b/bin/cli/commands/serve.mjs index eed761c693..2260127a38 100644 --- a/bin/cli/commands/serve.mjs +++ b/bin/cli/commands/serve.mjs @@ -10,9 +10,16 @@ import { isTermux } from "../../../scripts/build/postinstallSupport.mjs"; import { resolveMaxOldSpaceMb, calibrateHeapFallbackMb, + buildServerNodeOptions, + buildNodeHeapArgs, } from "../../../scripts/build/runtime-env.mjs"; +import { resolveTlsOptions } from "../../../scripts/dev/tls-options.mjs"; const __dirname = dirname(fileURLToPath(import.meta.url)); + +// URL scheme for the "OmniRoute is running" banner — flipped to https when +// opt-in TLS (#5242) is active. Process-scoped: one `serve` run = one scheme. +let urlScheme = "http"; const ROOT = join(__dirname, "..", "..", ".."); // The standalone bundle ships in `dist/` (since the build-output-isolation // refactor). Fall back to the legacy `app/` location so an upgrade over a @@ -39,6 +46,14 @@ export function registerServe(program) { .option("--max-restarts ", t("serve.max_restarts"), parseInt, 2) .option("--tray", t("serve.tray") || "Show system tray icon (desktop only)") .option("--no-tray", t("serve.no_tray") || "Disable system tray icon") + .option( + "--tls-cert ", + t("serve.tls_cert") || "Path to a TLS certificate (PEM) to serve HTTPS (also OMNIROUTE_TLS_CERT)" + ) + .option( + "--tls-key ", + t("serve.tls_key") || "Path to the TLS private key (PEM) to serve HTTPS (also OMNIROUTE_TLS_KEY)" + ) .action(async (opts) => { await runServe(opts); }); @@ -138,6 +153,11 @@ export async function runServe(opts = {}) { calibrateHeapFallbackMb(totalmem()) ); + // #5242: opt-in native HTTPS. CLI flags take precedence over env; the child + // server (server-ws.mjs) reads these and terminates TLS on the same listener. + const tlsCert = opts.tlsCert ?? process.env.OMNIROUTE_TLS_CERT; + const tlsKey = opts.tlsKey ?? process.env.OMNIROUTE_TLS_KEY; + const env = { ...process.env, OMNIROUTE_PORT: String(port), @@ -146,9 +166,19 @@ export async function runServe(opts = {}) { API_PORT: String(apiPort), HOSTNAME: process.env.HOSTNAME || "0.0.0.0", NODE_ENV: "production", - NODE_OPTIONS: `--max-old-space-size=${memoryLimit}`, + // #5238: preserve a user-set NODE_OPTIONS (incl. their own + // `--max-old-space-size=…`) instead of clobbering it with the calibrated + // default — mirror the Electron/standalone launchers. + NODE_OPTIONS: buildServerNodeOptions(process.env, memoryLimit), + ...(tlsCert ? { OMNIROUTE_TLS_CERT: tlsCert } : {}), + ...(tlsKey ? { OMNIROUTE_TLS_KEY: tlsKey } : {}), }; + // Validate the TLS pair up front so the operator sees a clear warning in the + // CLI (the child re-validates authoritatively). Drives the banner scheme; + // when null we fall through to identical plain-HTTP behavior as before. + urlScheme = resolveTlsOptions(env) ? "https" : "http"; + const isDaemon = opts.daemon === true; const useTray = opts.tray === true; @@ -174,7 +204,9 @@ export async function runServe(opts = {}) { } function runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort) { - const server = spawn("node", [`--max-old-space-size=${memoryLimit}`, serverJs], { + // #5238: skip the explicit CLI --max-old-space-size when the user pinned the + // heap via NODE_OPTIONS (a CLI arg would shadow/override their value). + const server = spawn("node", [...buildNodeHeapArgs(process.env, memoryLimit), serverJs], { cwd: APP_DIR, env, stdio: "ignore", @@ -183,12 +215,14 @@ function runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort) { writePidFile("server", server.pid); server.unref(); console.log(`\x1b[32m✔ OmniRoute started in background (PID: ${server.pid})\x1b[0m`); - console.log(` \x1b[1mDashboard:\x1b[0m http://localhost:${dashboardPort}`); - console.log(` \x1b[1mAPI Base:\x1b[0m http://localhost:${apiPort}/v1`); + console.log(` \x1b[1mDashboard:\x1b[0m ${urlScheme}://localhost:${dashboardPort}`); + console.log(` \x1b[1mAPI Base:\x1b[0m ${urlScheme}://localhost:${apiPort}/v1`); } function runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort, noOpen) { - const server = spawn("node", [`--max-old-space-size=${memoryLimit}`, serverJs], { + // #5238: skip the explicit CLI --max-old-space-size when the user pinned the + // heap via NODE_OPTIONS (a CLI arg would shadow/override their value). + const server = spawn("node", [...buildNodeHeapArgs(process.env, memoryLimit), serverJs], { cwd: APP_DIR, env, stdio: "pipe", @@ -312,7 +346,7 @@ async function maybeStartTray(port, apiPort, supervisor) { const { initTray, isTraySupported } = await import("../tray/index.mjs"); if (!isTraySupported()) return; const { default: open } = await import("open").catch(() => ({ default: null })); - const dashboardUrl = `http://localhost:${port}`; + const dashboardUrl = `${urlScheme}://localhost:${port}`; const tray = await initTray({ port, onQuit: () => { @@ -339,8 +373,8 @@ async function maybeStartTray(port, apiPort, supervisor) { } async function onReady(dashboardPort, apiPort, noOpen) { - const dashboardUrl = `http://localhost:${dashboardPort}`; - const apiUrl = `http://localhost:${apiPort}`; + const dashboardUrl = `${urlScheme}://localhost:${dashboardPort}`; + const apiUrl = `${urlScheme}://localhost:${apiPort}`; console.log(` \x1b[32m✔ OmniRoute is running!\x1b[0m diff --git a/bin/cli/locales/en.json b/bin/cli/locales/en.json index a4cfff165c..9d436d1d79 100644 --- a/bin/cli/locales/en.json +++ b/bin/cli/locales/en.json @@ -241,7 +241,9 @@ "no_recovery": "Disable auto-restart on crash (debugging mode)", "max_restarts": "Max crash restarts within 30s before giving up (default: 2)", "tray": "Show system tray icon (desktop only, opt-in)", - "no_tray": "Disable system tray icon" + "no_tray": "Disable system tray icon", + "tls_cert": "Path to a TLS certificate (PEM) to serve HTTPS (also OMNIROUTE_TLS_CERT)", + "tls_key": "Path to the TLS private key (PEM) to serve HTTPS (also OMNIROUTE_TLS_KEY)" }, "backup": { "title": "Backup", diff --git a/bin/cli/runtime/processSupervisor.mjs b/bin/cli/runtime/processSupervisor.mjs index ff201cb091..63f2a9912f 100644 --- a/bin/cli/runtime/processSupervisor.mjs +++ b/bin/cli/runtime/processSupervisor.mjs @@ -8,6 +8,7 @@ import { computeRestartDelayMs, waitUntilPortFree, } from "./supervisorPolicy.mjs"; +import { buildNodeHeapArgs } from "../../../scripts/build/runtime-env.mjs"; const CRASH_LOG_LINES = 50; @@ -30,7 +31,11 @@ export class ServerSupervisor { this.crashLog = []; const showLog = process.env.OMNIROUTE_SHOW_LOG === "1"; - this.child = spawn("node", [`--max-old-space-size=${this.memoryLimit}`, this.serverPath], { + // #5238: skip the explicit CLI --max-old-space-size when the user pinned the + // heap via NODE_OPTIONS (a CLI arg would shadow/override their value). The + // calibrated heap is already carried by env.NODE_OPTIONS either way. + const heapArgs = buildNodeHeapArgs(process.env, this.memoryLimit); + this.child = spawn("node", [...heapArgs, this.serverPath], { cwd: dirname(this.serverPath), env: this.env, stdio: showLog ? "inherit" : ["ignore", "ignore", "pipe"], diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 700c30c2f2..5890dfa4ba 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -1,5 +1,6 @@ { "_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.", + "_rebaseline_2026_06_29_v3841_release": "callLogs 985->997 + chat 1632->1635 + chatHelpers 811->842 (#5351 opencode visible-rotation-logs/ProxyEgress/SQL-vars), base.ts 1475->1497 + openai-to-claude 805->823 (#5352 thinking hydrate/redacted-replay + #5342 empty-messages guard), accountFallback 1777->1783 (#5346 402 auto-disable depleted key) + account-fallback-service.test 1569->1572 (#5346 test), provider-validation-specialty.test 2801->2843 (#5358 grok cf_clearance + #5337 gemini catalog). Cycle-close drift from merged campaign PRs absorbed at release close per generate-release Phase 0 (the PR->release fast-gates do NOT run check:file-size); all irreducible chokepoint/feature growth next to existing branches, covered by per-PR tests. Structural shrink tracked under decomposition roadmap #3501.", "_rebaseline_2026_06_26_v3838_ownerprs_batch": "Lote /review-prs v3.8.38 (PRs do dono + contribuidores): src/lib/db/providers.ts 1093->1107 (+14 = #5121 cookie-dedup branch extraido para o helper findExistingCookieConnection — a extracao reduz a complexidade ciclomatica de createProviderConnection mas adiciona ~14 linhas ao arquivo; trade-off consciente complexity-vs-file-size) e src/lib/usage/usageHistory.ts 934->983 (+49 = #4940 dedup guard SELECT-before-INSERT + endpoint backfill + scheduleStatsEvent debounce). Crescimento de feature/fix legitimo recem-TDD'd; o fast-path PR->release nao roda check:file-size, entao o ramo acumula sem rebaselinar. Reducao estrutural fica como debt (#3501).", "_rebaseline_2026_06_26_relgreen_db_test": "Release-green follow-up: tests/unit/db-core-init.test.ts 867->877 (+10 = the invalid-DATA_DIR test now captures the rejection and asserts both Error type AND message — restores net-neutral assert count after #5117's consolidation, satisfying check:test-masking — instead of a single assert.rejects).", "_rebaseline_2026_06_26_5074_fusion_editor": "PR #5074 own growth: src/app/(dashboard)/dashboard/combos/page.tsx 4485->4594 (+109 = the Fusion judgeModel + fusionTuning editor block — text/number inputs wired through updateFusionTuning, schema-validated) and tests/unit/combo-config.test.ts 800->881 (testFrozen add: 5 new Fusion-config schema tests). Cohesive UI block at the strategy-conditional editor; combos/page.tsx structural shrink tracked in #3501. Covered by combo-config.test.ts.", @@ -127,7 +128,7 @@ "_rebaseline_pr1043_minimax_tts": "Upstream port decolua/9router#1043 (toanalien) own growth: audioSpeech.ts 965->1061 (+96). Adds MiniMax T2A v2 TTS dispatch (handleMinimaxSpeech + hexToBytes helper) — provider entry was already in audioRegistry (format: minimax-tts) but no handler existed, falling through to the OpenAI-compatible default that fails (T2A has custom shape + hex-encoded audio + base_resp envelope). New branch sits next to the other inline provider branches (xiaomi-mimo, coqui, tortoise, aws-polly) — extracting would just create indirection. Covered by tests/unit/minimax-tts-1043.test.ts (3 tests, GREEN: success, base_resp error, invalid-hex).", "open-sse/config/providerRegistry.ts": 4731, "open-sse/executors/antigravity.ts": 1806, - "open-sse/executors/base.ts": 1475, + "open-sse/executors/base.ts": 1497, "open-sse/executors/chatgpt-web.ts": 2870, "open-sse/executors/claude-web.ts": 1057, "open-sse/executors/codex.ts": 1541, @@ -151,7 +152,7 @@ "open-sse/mcp-server/server.ts": 1555, "open-sse/mcp-server/tools/advancedTools.ts": 1118, "_rebaseline_2026_06_27_5193_antigravity_basered": "Base-red (pre-existing release drift, fast-gate PR->release skips check:file-size): accountFallback.ts 1773->1777 and src/app/api/providers/[id]/test/route.ts 924->940 were already over their frozen caps on release/v3.8.39 independent of any antigravity change. Owner chose to rebaseline (keep the documented issue-reference comments #1846/#1449/#347 etc.) rather than accept the contributor comment-stripping in #5200/#5198. Reverted #5200 to restore the comments; bumped these two frozen caps to the actual base sizes. No logic change.", - "open-sse/services/accountFallback.ts": 1777, + "open-sse/services/accountFallback.ts": 1783, "open-sse/services/batchProcessor.ts": 828, "open-sse/services/browserBackedChat.ts": 850, "open-sse/services/claudeCodeCompatible.ts": 1202, @@ -229,7 +230,7 @@ "src/lib/providers/validation.ts": 4523, "src/lib/resilience/settings.ts": 841, "src/lib/tailscaleTunnel.ts": 1202, - "src/lib/usage/callLogs.ts": 985, + "src/lib/usage/callLogs.ts": 997, "src/lib/usage/providerLimits.ts": 955, "src/lib/usage/usageHistory.ts": 988, "_rebaseline_2026_06_27_5193_5203_antigravity_oauthmodal": "Antigravity remote-login own growth: OAuthModal.tsx 960->969 (gate units). #5193 (+~4: remote paste instruction shown for all remote incl. Google + its rationale comment) and #5203 (+~5: handleManualSubmit credential-blob branch + button guard; submit logic extracted to oauthBlobSubmit.ts to minimize). Frozen set to the SUM so either merge order passes. Cohesive at the existing manual-submit chokepoint.", @@ -243,20 +244,22 @@ "src/shared/services/cliRuntime.ts": 1090, "src/shared/validation/schemas.ts": 2523, "_rebaseline_2026_06_28_5275_correlation_id_extract": "Extraction of the safe CorrelationId subset of #5275 (hartmark) — request correlation id stored in call_logs (migration 109) and returned via the X-Correlation-Id response header, WITHOUT the combo/resilience or build/lazy-loading changes (those stay in #5275). Own growth: callLogs.ts 975->985 (correlation_id column on CallLogSummaryRow + read/map), usageHistory.ts 983->988 (correlationId metadata normalize), chat.ts 1575->1632 (withCorrelationId response wiring + combo-failure log carrying correlationId), chatHelpers.ts new 811 (withCorrelationId helper + reqId threading; was 7911052 (+48, buildCompatibleProviderGroups partition unit test). Fast-gate PR->release does not run check:file-size, so this surfaced post-merge.", "tests/unit/providers-page-utils.test.ts": 1052, "tests/unit/reasoning-cache.test.ts": 980, diff --git a/config/quality/quality-baseline.json b/config/quality/quality-baseline.json index d0c47ed382..06616fef14 100644 --- a/config/quality/quality-baseline.json +++ b/config/quality/quality-baseline.json @@ -2,8 +2,9 @@ "_comment": "Catraca de qualidade. 'down' = nao pode aumentar; 'up' = nao pode cair. Atualize via 'npm run quality:ratchet -- --update' (somente quando melhora). Cada valor e um numero REAL medido, nunca um chute. Cobertura entra na Fase 4 a partir de um run de cobertura mergeada no CI.", "metrics": { "eslintWarnings": { - "value": 4103, + "value": 4116, "direction": "down", + "_rebaseline_2026_06_29_v3841_release": "4103->4116 (+13). v3.8.41 cycle drift surfaced by the release-green collect (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's 52 commits — relay backend #5315, gemini catalog #5337, services dashboard #5299, empty-Claude-messages guard #5342, thinking-budget/redacted-replay + marker opt-out #5312/#5352/#5367, opencode proxy-pool + observability #5217/#5370/#5351, cors + HTTPS-serve #5242/#5360/#5361, grok cf_clearance #5350/#5358, oauth/chatgpt-web/routing/cli/dashboard/rerank #5326/#5240/#5239/#5238/#5264/#5332, partially offset by the dead-code sweep #5321-#5371). Trust-but-verify: measured 4116 via `npm run quality:collect` on the working tree INCLUDING my reconciliation (CHANGELOG/i18n/README/env docs + baselines) AND the lint-fix in useServiceLogs.ts — that fix REMOVES a setState-in-effect ERROR (eslintErrors stays 0) and adds an `open` listener with no `any`/unused, contributing 0 warnings; all +13 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.", "_rebaseline_2026_06_29_v3840_release": "4090->4103 (+13). v3.8.40 cycle drift surfaced by the release-green pre-flight + the release PR Quality Ratchet (the ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's ~57 commits — compression roadmap relevance/hard-budget/memoization/transparency/saliency/splitter/tool_search/RTK/QuantumLock #5289/#5288/#5286/#5284/#5285/#5283/#5269/#5268/#5260, ~20 SSE/translator/combo fixes #5248/#5250/#5254/#5261/#5255/#5273/#5258, M365 Copilot provider #5302, public-origin centralization #5278). Trust-but-verify: measured 4103 locally via `npm run quality:collect` on the release tip INCLUDING my reconciliation commits (CHANGELOG + main merge + the 2 regression test fixes 165c823f5) — the test fixes add 0 `any`/warnings (health-autopilot added a NextRequest import + asserts; chat-pipeline changed one Accept string + a comment), so all +13 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.", "_rebaseline_2026_06_28_v3839_release": "4002->4090 (+88). v3.8.39 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's 40 commits — antigravity remote-login + quota-family #5203/#5180/#5193, compression CCR-retrieve + TOON encoder #5187/#5163, ~20 SSE/translator/responses fixes #5156/#5154/#5197/#5204/#5158/#5123/#5166, proxy/health hardening #5202/#5208/#5209/#5201 from @KooshaPari, combo quota-share/context-relay E2E tests #5179/#5168/#5195). Trust-but-verify: this release-finalize working tree touches ONLY CHANGELOG.md, docs/i18n/*/CHANGELOG.md mirrors, README.md and these baselines — 0 production-code change, so all +88 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.", "_rebaseline_2026_06_27_v3838_release": "3987->4002 (+15). v3.8.38 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle's ~78 commits — provider adds Factory/Grok-Build/ZenMux-Free/Alibaba-video, ~30 SSE/translator/diagnostics fixes, compression fidelity-gate + playground #5080/#5143, Fusion editor #5074, salvage batches #5138/#5141). Trust-but-verify: this release-finalize working tree touches ONLY CHANGELOG.md, docs/i18n/*/CHANGELOG.md mirrors, README.md and these baselines — 0 production-code change, so all +15 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.", @@ -101,16 +102,17 @@ "_rebaseline_2026_06_28_v3839_release": "78.4 -> 77.5 (-0.9, beyond the 0.5 eps). v3.8.39 cycle drift surfaced ONLY on the release PR (i18n-ui-coverage does NOT run on PR->release fast-gates). The cycle added new UI strings (compression studio TOON A/B table, antigravity remote-login dashboard field, amber warning icon) to the en denominator faster than the 37 non-en locales were translated; those locales need `npm run i18n:run` with OMNIROUTE_TRANSLATION_API_KEY (unavailable locally) — same precedent as _rebaseline_2026_06_18_v3828_cycle_close + _quality_rebaseline_2026_06_20_ci_ratchet. Measured by CI collect-metrics (run 28317145160) = 77.5. My release-finalize tree changes no src/i18n/messages/*.json. Tightening is tracked as follow-up (run i18n:run with creds)." }, "deadExports": { - "value": 346, + "value": 310, "direction": "down", "dedicatedGate": true, "_rebaseline_2026_06_27_v3838_release": "345->346 (+1). v3.8.38 cycle drift surfaced by the release-green pre-flight (Quality Ratchet does NOT run on PR->release fast-gates). Net +1 inherited from this cycle's feature/fix merges (new executors/providers, compression fidelity-gate module) minus #5138's removal of dead legacy store modules. Release-finalize working tree touches ONLY CHANGELOG.md + i18n mirrors + README + baselines — 0 production-code change. Structural cleanup tracked as debt.", "_rebaseline_2026_06_26_v3837_release": "343->345. v3.8.37 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings/complexity accrued unmeasured across this cycle's 76 commits — provider adds DGrid/Pioneer/xAI, headroom proxy lifecycle #4649, ~50 SSE/translator fixes, Engine Combos #5062). Trust-but-verify: this release-finalize working tree touches ONLY CHANGELOG.md, docs/i18n/*/CHANGELOG.md mirrors, and these baselines — 0 production-code change, so all drift is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle." }, "cognitiveComplexity": { - "value": 841, + "value": 842, "direction": "down", "dedicatedGate": true, + "_rebaseline_2026_06_29_v3841_release": "841->842 (+1). v3.8.41 cycle drift surfaced by the release-PR Quality Ratchet (cognitive-complexity does NOT run on PR->release fast-gates). The Phase-0 pre-flight measured 840 on the pre-campaign tip; the campaign's +34 later commits (thinking-budget/marker #5312/#5352/#5367, opencode proxy-pool/observability #5217/#5370/#5351, cors/HTTPS #5242/#5360/#5361, grok #5350/#5358, oauth/routing/cli #5326/#5239/#5238) added +2 net. My release-finalize changes are docs/baselines + a lint-fix (useServiceLogs open-listener), a test alignment (encryption.spec) and a pack-allowlist entry — all complexity-neutral. Structural shrink tracked in #3501.", "_rebaseline_2026_06_27_v3838_release": "833->841 (+8). v3.8.38 cycle drift surfaced by the release-green pre-flight (cognitive-complexity does NOT run on PR->release fast-gates). Inherited drift from this cycle's ~78 feature/fix merges (compression fidelity-gate #5143, SSE/streaming hardening #5124/#5108/#5085, resilience #5093, quota keepalive #5102, contributor provider/translator branches). god-file decomposition #3501 is complexity-neutral. Release-finalize working tree touches ONLY CHANGELOG.md + i18n mirrors + README + baselines — 0 production-code change. Structural shrink tracked in #3501.", "_rebaseline_2026_06_25_v3836_release": "801→816 (+15) — v3.8.36 cycle drift surfaced by the fix-PR #5029 CI (cognitive-complexity does NOT run on PR→release fast-gates, and the Quality Ratchet job was SKIPPED on the release PR #4854 itself, so the +15 from this cycle's 137 commits accrued unmeasured). Measured locally = 816 (identical to the CI Cognitive complexity ratchet). Drift from legit cycle features (Quota-Share Fase 2/3, task-aware/Fusion combo, contributor provider/translator branches); god-file decomposition #3501 is complexity-neutral. This fix-PR touches only pack-artifact-policy.ts + a test + the 2 baseline JSONs — contributes 0. Structural shrink tracked in #3501.", "_rebaseline_2026_06_22_v3834_release": "797→801 (+4) — v3.8.34 cycle drift surfaced by the release-green pre-flight (cognitive-complexity does NOT run on PR→release fast-gates). Verified my release-finalize working tree touches ONLY CHANGELOG.md (git status: 0 code changes), so all +4 is inherited contributor drift from this cycle's parallel-session merges. Structural shrink tracked in #3501.", diff --git a/docs/architecture/AUTHZ_GUIDE.md b/docs/architecture/AUTHZ_GUIDE.md index 3944c007e4..4f10555901 100644 --- a/docs/architecture/AUTHZ_GUIDE.md +++ b/docs/architecture/AUTHZ_GUIDE.md @@ -163,7 +163,9 @@ write:resilience, pricing:write, read:cache, write:cache, read:compression, write:compression, read:proxies ``` -Preset bundles (`MCP_SCOPE_PRESETS`): `readonly`, `full`, `monitor`, `agent`. Use `hasRequiredScopes(granted, toolName)` and `getMissingScopes()` for enforcement inside MCP handlers. +Scope enforcement in `open-sse/mcp-server/server.ts` passes each tool's scope list into +`evaluateToolScopes()` after `resolveCallerScopeContext()` resolves scopes from MCP auth info, +request metadata, or `OMNIROUTE_MCP_SCOPES`. ## Auth Required Toggle diff --git a/docs/i18n/ar/CHANGELOG.md b/docs/i18n/ar/CHANGELOG.md index 087e64ebf6..e22065efeb 100644 --- a/docs/i18n/ar/CHANGELOG.md +++ b/docs/i18n/ar/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/az/CHANGELOG.md b/docs/i18n/az/CHANGELOG.md index d583b8d582..ecc176e5b9 100644 --- a/docs/i18n/az/CHANGELOG.md +++ b/docs/i18n/az/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/bg/CHANGELOG.md b/docs/i18n/bg/CHANGELOG.md index d583b8d582..ecc176e5b9 100644 --- a/docs/i18n/bg/CHANGELOG.md +++ b/docs/i18n/bg/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/bn/CHANGELOG.md b/docs/i18n/bn/CHANGELOG.md index 7adb5f70bb..89392fec20 100644 --- a/docs/i18n/bn/CHANGELOG.md +++ b/docs/i18n/bn/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/cs/CHANGELOG.md b/docs/i18n/cs/CHANGELOG.md index e59a3dbca0..cf020753f9 100644 --- a/docs/i18n/cs/CHANGELOG.md +++ b/docs/i18n/cs/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/da/CHANGELOG.md b/docs/i18n/da/CHANGELOG.md index 9bbabe6101..5920fac2cd 100644 --- a/docs/i18n/da/CHANGELOG.md +++ b/docs/i18n/da/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/de/CHANGELOG.md b/docs/i18n/de/CHANGELOG.md index 45c91a5b13..d683af421a 100644 --- a/docs/i18n/de/CHANGELOG.md +++ b/docs/i18n/de/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/es/CHANGELOG.md b/docs/i18n/es/CHANGELOG.md index fcff02f64b..41f43f1b47 100644 --- a/docs/i18n/es/CHANGELOG.md +++ b/docs/i18n/es/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/fa/CHANGELOG.md b/docs/i18n/fa/CHANGELOG.md index aee9735728..3a25927103 100644 --- a/docs/i18n/fa/CHANGELOG.md +++ b/docs/i18n/fa/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/fi/CHANGELOG.md b/docs/i18n/fi/CHANGELOG.md index 49842588ea..76f7b06ee8 100644 --- a/docs/i18n/fi/CHANGELOG.md +++ b/docs/i18n/fi/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/fr/CHANGELOG.md b/docs/i18n/fr/CHANGELOG.md index 825c34cc1d..1d6839537f 100644 --- a/docs/i18n/fr/CHANGELOG.md +++ b/docs/i18n/fr/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/gu/CHANGELOG.md b/docs/i18n/gu/CHANGELOG.md index f6e5ead595..0d8478e3a9 100644 --- a/docs/i18n/gu/CHANGELOG.md +++ b/docs/i18n/gu/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/he/CHANGELOG.md b/docs/i18n/he/CHANGELOG.md index d181bc7fef..05516caf8f 100644 --- a/docs/i18n/he/CHANGELOG.md +++ b/docs/i18n/he/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/hi/CHANGELOG.md b/docs/i18n/hi/CHANGELOG.md index 2795959e41..4c0f703600 100644 --- a/docs/i18n/hi/CHANGELOG.md +++ b/docs/i18n/hi/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/hu/CHANGELOG.md b/docs/i18n/hu/CHANGELOG.md index ae0251f78e..8a87ba1904 100644 --- a/docs/i18n/hu/CHANGELOG.md +++ b/docs/i18n/hu/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/id/CHANGELOG.md b/docs/i18n/id/CHANGELOG.md index 1f27dc2b56..f7587dca58 100644 --- a/docs/i18n/id/CHANGELOG.md +++ b/docs/i18n/id/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/in/CHANGELOG.md b/docs/i18n/in/CHANGELOG.md index 2ce2ff474b..b97ecec8c5 100644 --- a/docs/i18n/in/CHANGELOG.md +++ b/docs/i18n/in/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/it/CHANGELOG.md b/docs/i18n/it/CHANGELOG.md index e0f91f9534..0ef8606a5f 100644 --- a/docs/i18n/it/CHANGELOG.md +++ b/docs/i18n/it/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/ja/CHANGELOG.md b/docs/i18n/ja/CHANGELOG.md index a7a5f7369b..9d91f2bd1f 100644 --- a/docs/i18n/ja/CHANGELOG.md +++ b/docs/i18n/ja/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/ko/CHANGELOG.md b/docs/i18n/ko/CHANGELOG.md index 2faae8fb88..c340b15326 100644 --- a/docs/i18n/ko/CHANGELOG.md +++ b/docs/i18n/ko/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/mr/CHANGELOG.md b/docs/i18n/mr/CHANGELOG.md index d2ff5aec98..dcf5606199 100644 --- a/docs/i18n/mr/CHANGELOG.md +++ b/docs/i18n/mr/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/ms/CHANGELOG.md b/docs/i18n/ms/CHANGELOG.md index 459c22356a..9b1748c258 100644 --- a/docs/i18n/ms/CHANGELOG.md +++ b/docs/i18n/ms/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/nl/CHANGELOG.md b/docs/i18n/nl/CHANGELOG.md index f5a1772f41..2e901bb2e2 100644 --- a/docs/i18n/nl/CHANGELOG.md +++ b/docs/i18n/nl/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/no/CHANGELOG.md b/docs/i18n/no/CHANGELOG.md index 7fe8d5831b..835093d171 100644 --- a/docs/i18n/no/CHANGELOG.md +++ b/docs/i18n/no/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/phi/CHANGELOG.md b/docs/i18n/phi/CHANGELOG.md index d8b0701603..8dd1422cc5 100644 --- a/docs/i18n/phi/CHANGELOG.md +++ b/docs/i18n/phi/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/pl/CHANGELOG.md b/docs/i18n/pl/CHANGELOG.md index 87cdda0529..d60439272e 100644 --- a/docs/i18n/pl/CHANGELOG.md +++ b/docs/i18n/pl/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/pt-BR/CHANGELOG.md b/docs/i18n/pt-BR/CHANGELOG.md index 22fb674576..5fac560308 100644 --- a/docs/i18n/pt-BR/CHANGELOG.md +++ b/docs/i18n/pt-BR/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/pt/CHANGELOG.md b/docs/i18n/pt/CHANGELOG.md index 322ba2ca8f..b5d16e5bca 100644 --- a/docs/i18n/pt/CHANGELOG.md +++ b/docs/i18n/pt/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/ro/CHANGELOG.md b/docs/i18n/ro/CHANGELOG.md index 4f202414ec..33006a6a12 100644 --- a/docs/i18n/ro/CHANGELOG.md +++ b/docs/i18n/ro/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/ru/CHANGELOG.md b/docs/i18n/ru/CHANGELOG.md index 22f81c0a07..b5c053a326 100644 --- a/docs/i18n/ru/CHANGELOG.md +++ b/docs/i18n/ru/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/sk/CHANGELOG.md b/docs/i18n/sk/CHANGELOG.md index 3f524876c7..90acb8890f 100644 --- a/docs/i18n/sk/CHANGELOG.md +++ b/docs/i18n/sk/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/sv/CHANGELOG.md b/docs/i18n/sv/CHANGELOG.md index aeb94c25f2..e240458dde 100644 --- a/docs/i18n/sv/CHANGELOG.md +++ b/docs/i18n/sv/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/sw/CHANGELOG.md b/docs/i18n/sw/CHANGELOG.md index e4cfb9ed89..9b6646f53c 100644 --- a/docs/i18n/sw/CHANGELOG.md +++ b/docs/i18n/sw/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/ta/CHANGELOG.md b/docs/i18n/ta/CHANGELOG.md index 23d1e60dbe..e276f3de96 100644 --- a/docs/i18n/ta/CHANGELOG.md +++ b/docs/i18n/ta/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/te/CHANGELOG.md b/docs/i18n/te/CHANGELOG.md index a5799a736c..bd39d91672 100644 --- a/docs/i18n/te/CHANGELOG.md +++ b/docs/i18n/te/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/th/CHANGELOG.md b/docs/i18n/th/CHANGELOG.md index c5451017ce..e4fc1fb956 100644 --- a/docs/i18n/th/CHANGELOG.md +++ b/docs/i18n/th/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/tr/CHANGELOG.md b/docs/i18n/tr/CHANGELOG.md index f35465af59..cd298b065b 100644 --- a/docs/i18n/tr/CHANGELOG.md +++ b/docs/i18n/tr/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/uk-UA/CHANGELOG.md b/docs/i18n/uk-UA/CHANGELOG.md index e7f2e2114c..c792dc6c0f 100644 --- a/docs/i18n/uk-UA/CHANGELOG.md +++ b/docs/i18n/uk-UA/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/ur/CHANGELOG.md b/docs/i18n/ur/CHANGELOG.md index 0009df73f8..7d2198d032 100644 --- a/docs/i18n/ur/CHANGELOG.md +++ b/docs/i18n/ur/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/vi/CHANGELOG.md b/docs/i18n/vi/CHANGELOG.md index 5fe434d6ed..a54bc6d3b4 100644 --- a/docs/i18n/vi/CHANGELOG.md +++ b/docs/i18n/vi/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/zh-CN/CHANGELOG.md b/docs/i18n/zh-CN/CHANGELOG.md index 3e9214c048..a163cd287b 100644 --- a/docs/i18n/zh-CN/CHANGELOG.md +++ b/docs/i18n/zh-CN/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/i18n/zh-TW/CHANGELOG.md b/docs/i18n/zh-TW/CHANGELOG.md index 798ad641bf..efd3574289 100644 --- a/docs/i18n/zh-TW/CHANGELOG.md +++ b/docs/i18n/zh-TW/CHANGELOG.md @@ -6,6 +6,38 @@ ## [3.8.31] — 2026-06-20 +## [3.8.41] — 2026-06-29 + +### ✨ New Features + +- **feat(relay): selectable relay backend (TS / Bifrost / `auto`)** — the OpenAI-compatible relay endpoint can now route its hot path through a native Bifrost sidecar without clients changing URLs. `OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` = `ts | bifrost | auto`: defaults to the existing TypeScript relay; `auto` selects Bifrost when `BIFROST_BASE_URL` is set (and `BIFROST_ENABLED` ≠ `0`) and falls back to TS automatically if the sidecar is unreachable; `bifrost` keeps strict failure behavior. Auth, per-IP/token rate limits, prompt-injection checks, and model allowlists still run in the Next relay route before dispatch (control plane stays in the app); responses carry `X-Routing-Backend` / `X-Routing-Fallback`. Regression guards: `tests/unit/api/v1/relay-routing-backend.test.ts`, `tests/unit/api/v1/bifrost-sidecar.test.ts`. ([#5315](https://github.com/diegosouzapw/OmniRoute/pull/5315), #5316 — thanks @KooshaPari) + +### 🔧 Bug Fixes + +- **translator (claude):** synthesize a minimal `user` turn when an OpenAI→Claude request carries **only** `system`/`developer` messages, so the request stops failing with `[400]: messages: at least one message is required`. `openaiToClaudeRequest` hoists every system/developer turn into Claude's top-level `system` field and filters them out of `messages`; an all-system input (OpenCode compaction / title-generation requests) left `messages: []`, which the Messages API rejects — surfacing in OpenCode as a mid-task `stream error` that drops the conversation. The guard fires only when `messages` would otherwise be empty (system instructions still drive the response), so non-empty requests are unaffected. ([#5342](https://github.com/diegosouzapw/OmniRoute/pull/5342) — thanks @wild-feather) +- **providers (gemini):** drop retired Google AI Studio model ids and align the catalog to what the live GenAI API actually serves (verified 2026-06-29 against the official deprecations page). Removes long-retired `gemini-1.5-pro`/`gemini-1.5-flash`, the shut-down `gemini-2.0-flash`/`gemini-2.0-flash-lite`, and dead experimentals; renames `gemini-3.1-flash-lite-preview` → the GA `gemini-3.1-flash-lite`; swaps the retired `text-embedding-004` for the live `gemini-embedding-001`/`gemini-embedding-2`; and adds graceful `modelDeprecation` forwards so legacy/renamed ids redirect to the GA model instead of 404ing. Native AI-Studio-direct image/video/music registration is intentionally out of scope (needs real executor work; those models stay reachable via Antigravity/Vertex/aggregators). ([#5337](https://github.com/diegosouzapw/OmniRoute/pull/5337) — thanks @backryun) +- **services (dashboard):** fix the embedded-services dashboard failures (#5298) — service supervisors are now lazily initialized from `/api/services/[name]/logs` so `cliproxy`/`9router` logs no longer 404 before bootstrap registers a supervisor; lifecycle buttons send JSON (empty install bodies default to `version: "latest"`, malformed JSON still returns `400 Invalid JSON body`); lifecycle and log-stream failures surface as actionable UI errors instead of silently showing no logs; Tailscale CGNAT `100.64.0.0/10` peers count as private-LAN local for local-only service access; a parent `/dashboard/context` → `/dashboard/context/settings` redirect stops RSC prefetch 404s; and `/api/v1/providers/{cliproxyapi,9router}/models` return synced embedded-service models instead of `invalid_provider`. ([#5299](https://github.com/diegosouzapw/OmniRoute/pull/5299), #5298 — thanks @KooshaPari) +- **thinking (claude):** fix three independent defects in Claude adaptive-thinking on the OpenAI-compatible path (Cursor → Claude OAuth). **(A)** the dashboard Thinking-Budget setting was dropped on every restart — `setThinkingBudgetConfig` was never called at boot, so a saved `{mode:"adaptive"…}` silently reverted to passthrough; it's now hydrated from settings in `server-init`. **(B)** the Claude executor force-injected adaptive thinking _after_ translation, ignoring the operator's budget — it now honors `mode:"auto"` (strip) while keeping the default (passthrough) behavior byte-identical so native Claude Code is unaffected, and remaps an operator `thinking.type:"enabled"` to the `adaptive` shape Opus 4.7/4.8 require (`enabled` → 400). **(D)** on replay, signature-less `reasoning_content` was reconstructed as a `thinking` block carrying a fabricated signature → Anthropic `400 "Invalid signature in thinking block"`; it now emits a signature-less `redacted_thinking` block (real signatures are still preserved verbatim). Regression guards: `tests/unit/thinking-budget-hydration-5312.test.ts`, `base-thinking-budget-config-5312.test.ts`, `openai-to-claude-redacted-replay-5312.test.ts` (existing #5123/#4479/#2454 suites stay green). The `` content-marker channel mismatch (RC-C, shared with #5245) is tracked as a follow-up pending a live Anthropic validation. ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312) — thanks @vitalNohj) +- **opencode (proxy pool):** the OpenCode Free per-account proxy modal now offers the global **Proxy Pool dropdown** (by-id reference) instead of forcing manual Host/Port/credentials on every account — Gap 1 of #5217. A **Saved / Custom** toggle: "Saved" picks a pre-saved proxy from `GET /api/settings/proxies` and stores `{fingerprint, proxyId}`, so updating that pool proxy applies to every account using it; "Custom" keeps the manual inputs (stored inline) as an escape hatch. Resolution happens server-side (`resolveAccountProxiesFromRegistry`) so the executor still receives a resolved proxy unchanged; existing inline entries keep working and an unknown/deleted `proxyId` degrades safely to direct. Regression guards: `tests/unit/noauth-proxy-resolution.test.ts`, `tests/unit/ui/noauth-account-card.test.tsx`. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) Gap 1 — thanks @daniij) +- **thinking (claude):** let reasoning_content-native clients (e.g. Cursor) opt out of the `` close-marker so it no longer leaks an orphan `` into visible `content` (RC-C of #5312, shared with #5245). The marker-suppression machinery already existed (UA allowlist, #5348) but Cursor's UA was deliberately excluded; this adds an explicit request header `x-omniroute-thinking-marker: off` (also `on`/`keep` to force-keep) that overrides the UA policy. With the header absent the behavior is byte-identical — Claude Code/Cursor-composer clients that scan `content` for the marker (#4633) still receive it. Regression guard: `tests/unit/think-close-marker-suppress-5245.test.ts` (#5123 case-b + #4479 stay green). ([#5312](https://github.com/diegosouzapw/OmniRoute/issues/5312), [#5245](https://github.com/diegosouzapw/OmniRoute/issues/5245) — thanks @vitalNohj, @wild-feather) +- **cors:** browser/Electron clients (e.g. Wayland AI) can now use OmniRoute as an OpenAI-compatible provider out-of-the-box. The token-authenticated API surface (`/v1/*`, `/v1beta/*`) now returns a permissive `Access-Control-Allow-Origin` (echoes the request `Origin`, `*` when absent) by default — matching 9router and the OpenAI-compatible ecosystem — so a renderer `fetch` can read the response instead of failing CORS-blocked as "site not found" / empty catalog (while `curl`, which sends no preflight, worked). This is **safe**: those routes auth via `Authorization`/`x-api-key` headers browsers never auto-attach (no credentialed-session/CSRF exposure), and `Access-Control-Allow-Credentials` is never paired with the echo/wildcard. Cookie-authed **MANAGEMENT/dashboard routes stay exactly fail-closed**; `CORS_ALLOW_ALL`/`CORS_ALLOWED_ORIGINS` still take precedence. Regression guards: `tests/unit/cors/origins.test.ts`, `tests/unit/authz/pipeline.test.ts`. (Bug 2 of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **grok-web:** forward the Cloudflare clearance cookies and stop mislabeling IP-reputation blocks as a bad cookie. "Check cookie" returned `Invalid SSO cookie` even with a valid, complete browser session — but the cookie parser was never the problem (it robustly extracts `sso`/`sso-rw` from a full DevTools header). Two real gaps fixed: **(1)** `buildGrokCookieHeader` now forwards `cf_clearance` and `__cf_bm` when pasted (it dropped them before; AIClient2API forwards them too) — strictly additive, a bare `sso` blob still yields exactly `sso=…`; **(2)** when the user supplied a `cf_clearance`, a 401 / invalid-credentials-403 from grok.com is now surfaced as an IP-reputation/anti-bot block (cf_clearance is IP+TLS+UA-pinned and can't be replayed from a different machine) instead of the misleading "Invalid SSO cookie — re-paste". A bare cookie with no clearance still gets the re-paste hint. Regression guards in `web-cookie-auth.test.ts` + `provider-validation-specialty.test.ts`. ([#5350](https://github.com/diegosouzapw/OmniRoute/issues/5350) — thanks @SeaXen) +- **cli (serve):** opt-in native **HTTPS/TLS** for `omniroute serve` — so strict-CSP Electron apps and browsers can reach OmniRoute over `https://` instead of plain `http://localhost`. Provide `--tls-cert --tls-key ` (or `OMNIROUTE_TLS_CERT`/`OMNIROUTE_TLS_KEY`) and the standalone server terminates TLS on the same listener (no extra port/proxy); WebSocket upgrade (live dashboard + `/v1` streaming) works over `wss://` unchanged since `https.Server extends http.Server`. With no TLS flags the HTTP path is byte-identical to before; only one of cert/key, or an unreadable path, logs a warning and stays HTTP (never half-enables, never crashes). Auto-generated self-signed certs for localhost are a follow-up; for now provide an explicit cert/key (or front OmniRoute with a TLS terminator). Regression guard: `tests/unit/tls-options.test.ts`. (Bug 1C of [#5242](https://github.com/diegosouzapw/OmniRoute/issues/5242) — thanks @jonlwheat2-gif) +- **opencode/observability:** make OpenCode Free account/proxy rotation visible and fix two real defects surfaced alongside it. **(1)** the per-request rotation selection log (`dispatch via account … through proxy …`) was `debug` (hidden at default `APP_LOG_LEVEL=info`) — promoted to `info` so the shuffle/cooldown lifecycle is auditable (token stays masked). **(2)** `[ProxyEgress]` reported `proxy=direct` even when an account proxy was applied, because the egress logger ran outside the executor's nested proxy context — the effective applied proxy is now captured (via an applied-proxy sink threaded through the proxy AsyncLocalStorage) and reflected in the egress log. **(3)** `[callLogs] too many SQL variables` — `deleteCallLogRowsByIds` deleted up to 5000 ids in one `IN (…)`, exceeding SQLite's ~999 bound-param cap and aborting log trimming/retention; ids are now chunked (≤500 per statement). Regression guards: `tests/unit/call-log-trim-sql-vars-5217.test.ts`, `apply-executor-proxy-info-5217.test.ts`, extended `opencode-proxy-rotation-4954.test.ts`. The Proxy Pool dropdown (by-id) UI (Gap 1) is a follow-up requiring browser validation. ([#5217](https://github.com/diegosouzapw/OmniRoute/issues/5217) — thanks @daniij) +- **chatgpt-web:** wire tool/function calling into the `chatgpt-web` provider. It was the only web-session executor that never read `body.tools` — both response builders hardcoded `finish_reason:"stop"` and emitted only content, so tool calls were silently dropped (the model answered in prose). It now uses the shared `webTools` prompt-emulation shim (a ``-contract system message + `{…}` response parsing) exactly like its 9 sibling executors (qwen-web, perplexity-web, …) — it was simply omitted from the #3259 rollout. Tool mode buffers and emits `tool_calls` + `finish_reason:"tool_calls"` (gated off the image-gen path); plain chat is unchanged. Regression guard: `tests/unit/chatgpt-web-tools-5240.test.ts`. ([#5240](https://github.com/diegosouzapw/OmniRoute/issues/5240) — thanks @Rougler) +- **oauth/dashboard:** fix the persistent/false Antigravity "Token Expired" badge (continuation of #3679/#3850). Two causes: **(1)** new OAuth connections never set `tokenExpiresAt` (only `expiresAt`), so the dashboard badge — which prefers `tokenExpiresAt || expiresAt` — fell back to the original grant clock and could flash a false "Token Expired" until the first background refresh. Creation now mirrors `expiresAt` into `tokenExpiresAt` across all 5 OAuth create paths (a shared `buildOAuthConnectionCreatePayload`), consistent with every refresh path which already writes both. **(2)** when a refresh-capable connection has no usable refresh token, the health-check sweep silently skipped it, leaving `testStatus="active"` forever while the cosmetic badge showed expired; it now surfaces a terminal `testStatus="expired"` ("needs re-auth"), tightly gated so it never clobbers non-refresh providers or already-terminal/cooldown states. Regression guards: `tests/unit/oauth-connection-tokenexpiresat-5326.test.ts`, `tests/unit/token-health-no-refresh-token-expired-5326.test.ts`. ([#5326](https://github.com/diegosouzapw/OmniRoute/issues/5326)) +- **routing:** auto-disable a depleted API key on upstream `402 "Insufficient account balance"` for API Key Round-Robin connections (multiple keys in one connection's `extraApiKeys`). The per-connection path already terminalized 402 (→ `credits_exhausted`), but the per-KEY health tracker (`recordKeyHealthStatus`) only recorded failures for `401`, so a 402-depleted key stayed in rotation and kept getting retried. Now a 402 marks the current key invalid immediately (terminal — balance won't recover mid-session) via a new `recordKeyTerminal`, so the rotator skips it and falls over to the next healthy key; the state persists across restarts. Also added `insufficient balance`/`insufficient_balance`/`insufficient account balance` to the credits-exhausted body signals so non-402 out-of-credit responses terminalize too. Regression guard: `tests/unit/key-health-402-disable-5239.test.ts`. ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) — thanks @muflifadla38) +- **cli:** `omniroute serve` no longer discards a user-set `NODE_OPTIONS=--max-old-space-size=…`. It used to unconditionally overwrite `NODE_OPTIONS` (and pass an explicit `--max-old-space-size` CLI arg) with the calibrated default, so a user who exported `--max-old-space-size=8192` still ran at the old cap and OOM'd (#5238 reporter set 8192, crashed at ~505MB). Now it mirrors the Electron and standalone launchers: if `NODE_OPTIONS` already pins the heap, that value wins (and the duplicate CLI arg is suppressed); otherwise the calibrated `--max-old-space-size` is appended, preserving unrelated flags. Regression guard: `tests/unit/serve-node-options-preserve-5238.test.ts`. (Defect C of [#5238](https://github.com/diegosouzapw/OmniRoute/issues/5238); the `b.mask`/OOM-root parts are tracked separately.) +- **dashboard:** restore the `{active}/{total} active` model-count badge in a provider's **Available Models** toolbar (provider detail page). It was dropped during the v3.8.13 god-file decomposition (#3327) — the `ModelVisibilityToolbar` still received `activeCount`/`totalCount` but they were orphaned as unused `_`-prefixed params and the rendering `` was never carried over (the `modelsActiveCount` i18n key stayed). Re-wired the existing props to the existing key; zero data-layer or i18n change. Regression guard: `modelVisibilityToolbarActiveCount.test.tsx`. ([#5264](https://github.com/diegosouzapw/OmniRoute/issues/5264)) +- **rerank:** `/v1/rerank` no longer rejects SiliconFlow and DeepInfra Qwen3-Reranker models with `400 "Invalid rerank model"` even though `/v1/models` lists them. The model-ID parser was never the problem (it already splits on the first slash, so `siliconflow/Qwen/Qwen3-Reranker-8B` parses correctly) — `siliconflow` and `deepinfra` were just missing from the rerank provider registry. Added both: SiliconFlow as Cohere-compatible, DeepInfra via a new `deepinfra` adapter (model in the URL path `POST /v1/inference/`, `{queries,documents}` request, positional `{scores}` response mapped to Cohere `results[]`). Regression guard: `tests/unit/rerank-providers-5332.test.ts`. ([#5332](https://github.com/diegosouzapw/OmniRoute/issues/5332) — thanks @maikokan) +- **authz/dashboard:** stop rejecting every dashboard mutation with `403 INVALID_ORIGIN` when the dashboard is reached over a LAN IP / non-localhost host. The origin-pinning check (#5278) only accepted the configured `*_PUBLIC_BASE_URL` (typically `http://localhost:20128`) plus the internal `request.url` origin — which Next.js standalone reports as the bind host, not the real `Host`. So opening the dashboard at e.g. `http://192.168.0.15:20128` made the browser's same-origin `Origin` match no candidate, and **every** POST/PUT/DELETE (save API key, save provider, test connection) failed while GETs still worked. Two fixes: **(a)** the request `Host` (or a trusted `X-Forwarded-Host`) is now accepted as a valid mutation origin, gated by two independent checks — the token-stamped socket peer must be loopback/private-LAN **and** the Host itself must be a loopback/private-LAN IP literal, so a DNS-rebinding domain (which classifies as `remote`) can never become a trusted origin and the protocol is pinned to the actual connection; **(b)** the `INVALID_ORIGIN` response now carries an actionable message (set `OMNIROUTE_PUBLIC_BASE_URL`) and the dashboard surfaces API error `.message` via a shared `extractApiErrorMessage` helper instead of rendering the raw error object. Regression guards: `tests/unit/authz/public-origin.test.ts` (direct LAN/loopback + DNS-rebinding defense), `tests/unit/api-error-message-5340.test.ts`. ([#5340](https://github.com/diegosouzapw/OmniRoute/issues/5340)) + +### 📝 Maintenance + +- **chore(dead-code):** repo-wide sweep of unused exported symbols and a matching dead-code baseline ratchet — trimmed unused exported helpers, validation/settings/encryption-config schemas, utility/domain/static-constant/formatting helpers, runtime test helpers, the request-timeout fetch wrapper, event-bus, semantic-cache (maintenance + expiry), correlation-middleware, MCP-scope, service-registry, build-profile, api-key-format, authz-class, models.dev-context, embedding-cache, provider-limits-scheduler, search-validator, webhook-example, agent-skills-repo-URL and command-code-auth-cleanup exports. Pure dead-code removal validated by `typecheck:core` (no remaining referencing site) — no behavior change. ([#5321](https://github.com/diegosouzapw/OmniRoute/pull/5321), [#5322](https://github.com/diegosouzapw/OmniRoute/pull/5322), [#5324](https://github.com/diegosouzapw/OmniRoute/pull/5324), [#5325](https://github.com/diegosouzapw/OmniRoute/pull/5325), [#5328](https://github.com/diegosouzapw/OmniRoute/pull/5328), [#5329](https://github.com/diegosouzapw/OmniRoute/pull/5329), [#5330](https://github.com/diegosouzapw/OmniRoute/pull/5330), [#5331](https://github.com/diegosouzapw/OmniRoute/pull/5331), [#5333](https://github.com/diegosouzapw/OmniRoute/pull/5333), [#5334](https://github.com/diegosouzapw/OmniRoute/pull/5334), [#5335](https://github.com/diegosouzapw/OmniRoute/pull/5335), [#5336](https://github.com/diegosouzapw/OmniRoute/pull/5336), [#5338](https://github.com/diegosouzapw/OmniRoute/pull/5338), [#5339](https://github.com/diegosouzapw/OmniRoute/pull/5339), [#5353](https://github.com/diegosouzapw/OmniRoute/pull/5353), [#5354](https://github.com/diegosouzapw/OmniRoute/pull/5354), [#5355](https://github.com/diegosouzapw/OmniRoute/pull/5355), [#5356](https://github.com/diegosouzapw/OmniRoute/pull/5356), [#5357](https://github.com/diegosouzapw/OmniRoute/pull/5357), [#5359](https://github.com/diegosouzapw/OmniRoute/pull/5359), [#5362](https://github.com/diegosouzapw/OmniRoute/pull/5362), [#5364](https://github.com/diegosouzapw/OmniRoute/pull/5364), [#5365](https://github.com/diegosouzapw/OmniRoute/pull/5365), [#5366](https://github.com/diegosouzapw/OmniRoute/pull/5366), [#5368](https://github.com/diegosouzapw/OmniRoute/pull/5368), [#5369](https://github.com/diegosouzapw/OmniRoute/pull/5369), [#5371](https://github.com/diegosouzapw/OmniRoute/pull/5371) — thanks @JxnLexn) + +--- + ## [3.8.40] — TBD _In development — bullets added per PR; finalized at release._ diff --git a/docs/openapi.yaml b/docs/openapi.yaml index c235458310..607817ddd6 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: OmniRoute API - version: 3.8.40 + version: 3.8.41 description: | OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible endpoint that routes requests to multiple AI providers with load balancing, diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index b38c372545..8c2f7043e9 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -1005,6 +1005,10 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy), | `BIFROST_STREAMING_ENABLED` | `true` | `src/app/api/v1/relay/chat/completions/bifrost/route.ts` | When true, the Bifrost sidecar route streams responses back via SSE through the gateway rather than the TS streaming executor. Set to `0` to force non-streaming JSON responses through the gateway. | | `BIFROST_TIMEOUT_MS` | `30000` | `src/app/api/v1/relay/chat/completions/bifrost/route.ts` | Per-request timeout when proxying to the Bifrost gateway (ms). On timeout the route returns the TS relay path via the `X-Bifrost-Fallback` header. | | `OMNIROUTE_BIFROST_KEY` | _(unset)_ | `src/app/api/v1/relay/chat/completions/bifrost/route.ts` | Alias for `BIFROST_API_KEY` (used by scripts that read the env via `OMNIROUTE_*`). Falls back to `BIFROST_API_KEY` when unset. | +| `OMNIROUTE_RELAY_BACKEND` | `ts` / `auto` | `src/app/api/v1/relay/chat/completions/routingBackend.ts` | Relay backend for `/api/v1/relay/chat/completions`: `ts \| bifrost \| auto`. `ts` = TypeScript relay (default when Bifrost unconfigured); `auto` selects Bifrost when `BIFROST_BASE_URL` is set and `BIFROST_ENABLED` ≠ `0`, with automatic TS fallback if the sidecar is unreachable; `bifrost` forces Bifrost (strict, no fallback). Auth/rate-limit/injection-guard/allowlist always run in the Next route first. Responses carry `X-Routing-Backend` / `X-Routing-Fallback`. | +| `RELAY_ROUTING_BACKEND` | _(unset)_ | `src/app/api/v1/relay/chat/completions/routingBackend.ts` | Accepted alias for `OMNIROUTE_RELAY_BACKEND` (same `ts \| bifrost \| auto` values). `OMNIROUTE_RELAY_BACKEND` takes precedence when both are set. | +| `OMNIROUTE_TLS_CERT` | _(unset)_ | `bin/cli/commands/serve.mjs` | Path to a PEM TLS certificate to serve `omniroute serve` over HTTPS (equivalent to `--tls-cert`). Must be paired with `OMNIROUTE_TLS_KEY`; the standalone server then terminates TLS on the same listener (`wss://` works unchanged). Unset → plain HTTP. Providing only one of cert/key, or an unreadable path, logs a warning and stays HTTP. | +| `OMNIROUTE_TLS_KEY` | _(unset)_ | `bin/cli/commands/serve.mjs` | Path to the PEM TLS private key for `omniroute serve` HTTPS (equivalent to `--tls-key`). Must be paired with `OMNIROUTE_TLS_CERT`. See `OMNIROUTE_TLS_CERT`. | | `OMNIROUTE_LOCAL_ENDPOINTS_ENABLED` | `0` | `src/lib/security/localEndpoints.ts` | Master switch for `/api/local/*` routes. When unset or `0`, all `/api/local/*` routes return 503 in production. Must be `1` in non-loopback deploys to enable the Redis launcher and similar 1-click local service starters. Belt-and-suspenders with `isLocalOnlyPath()` route-guard classification (`LOCAL_ONLY_API_PREFIXES` in `src/server/authz/routeGuard.ts`). | | `OMNIROUTE_LOCAL_ENDPOINTS_TOKEN` | _(unset)_ | `src/lib/security/localEndpoints.ts` | Bearer token for `/api/local/*` callers that aren't on loopback (e.g. the desktop app). When set, requests from non-loopback IPs must carry `Authorization: Bearer `. Required when `OMNIROUTE_LOCAL_ENDPOINTS_ENABLED=1` in non-loopback deployments. | | `OMNIROUTE_REDIS_CONTAINER_NAME` | `omniroute-redis` | `bin/cli/commands/redis.mjs` | Container name for the 1-click Redis launcher (`omniroute redis up`). Used by both the CLI and the `RedisLauncherPanel` GUI. | diff --git a/electron/package-lock.json b/electron/package-lock.json index 0fac1fade9..521a6ebb38 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute-desktop", - "version": "3.8.40", + "version": "3.8.41", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute-desktop", - "version": "3.8.40", + "version": "3.8.41", "license": "MIT", "dependencies": { "electron-updater": "^6.8.9" diff --git a/electron/package.json b/electron/package.json index f3c67c0184..1720820298 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "omniroute-desktop", - "version": "3.8.40", + "version": "3.8.41", "description": "OmniRoute Desktop Application", "main": "main.js", "author": { diff --git a/open-sse/config/embeddingRegistry.ts b/open-sse/config/embeddingRegistry.ts index 3063427a21..d7ba0777f5 100644 --- a/open-sse/config/embeddingRegistry.ts +++ b/open-sse/config/embeddingRegistry.ts @@ -205,7 +205,10 @@ export const EMBEDDING_PROVIDERS: Record = { baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai/embeddings", authType: "apikey", authHeader: "bearer", - models: [{ id: "text-embedding-004", name: "Text Embedding 004", dimensions: 768 }], + models: [ + { id: "gemini-embedding-2", name: "Gemini Embedding 2", dimensions: 768 }, + { id: "gemini-embedding-001", name: "Gemini Embedding 001", dimensions: 768 }, + ], }, "voyage-ai": { diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts index 27fb90524f..a25e1e363c 100644 --- a/open-sse/config/freeModelCatalog.data.ts +++ b/open-sse/config/freeModelCatalog.data.ts @@ -167,9 +167,8 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [ { provider: "gemini", modelId: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", monthlyTokens: 60000000, creditTokens: 0, freeType: "recurring-daily", poolKey: "gemini-free", tos: "caution" }, { provider: "gemini", modelId: "gemini-2.5-flash-lite", displayName: "Gemini 2.5 Flash-Lite", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-daily", poolKey: "gemini-free", tos: "caution" }, { provider: "gemini", modelId: "gemini-3-flash-preview", displayName: "Gemini 3 Flash Preview", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-daily", poolKey: "gemini-free", tos: "caution" }, - { provider: "gemini", modelId: "gemini-3.1-flash-lite-preview", displayName: "Gemini 3.1 Flash-Lite", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-daily", poolKey: "gemini-free", tos: "caution" }, + { provider: "gemini", modelId: "gemini-3.1-flash-lite", displayName: "Gemini 3.1 Flash-Lite", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-daily", poolKey: "gemini-free", tos: "caution" }, { provider: "gemini", modelId: "gemini-3.5-flash", displayName: "Gemini 3.5 Flash", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-daily", poolKey: "gemini-free", tos: "caution" }, - { provider: "gemini", modelId: "gemma-4", displayName: "Gemma 4 (open model)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-daily", poolKey: "gemini-free", tos: "caution" }, { provider: "github-models", modelId: "openai/gpt-4.1", displayName: "GPT-4.1 (Free)", monthlyTokens: 18000000, creditTokens: 0, freeType: "recurring-daily", poolKey: "github-models", tos: "caution" }, { provider: "github-models", modelId: "openai/gpt-4o", displayName: "GPT-4o (Free)", monthlyTokens: 18000000, creditTokens: 0, freeType: "recurring-daily", poolKey: "github-models", tos: "caution" }, { provider: "github-models", modelId: "openai/gpt-4o-mini", displayName: "GPT-4o Mini (Free)", monthlyTokens: 18000000, creditTokens: 0, freeType: "recurring-daily", poolKey: "github-models", tos: "caution" }, diff --git a/open-sse/config/providers/registry/gemini/index.ts b/open-sse/config/providers/registry/gemini/index.ts index 1c1f64b343..9f6dbbb70a 100644 --- a/open-sse/config/providers/registry/gemini/index.ts +++ b/open-sse/config/providers/registry/gemini/index.ts @@ -21,13 +21,6 @@ export const geminiProvider: RegistryEntry = { clientSecretDefault: resolvePublicCred("gemini_alt"), }, models: [ - { id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", toolCalling: true, supportsVision: true }, - { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite", - toolCalling: true, - supportsVision: true, - }, { id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview", @@ -41,14 +34,8 @@ export const geminiProvider: RegistryEntry = { supportsVision: true, }, { - id: "gemini-3-flash-lite-preview", - name: "Gemini 3 Flash Lite Preview", - toolCalling: true, - supportsVision: true, - }, - { - id: "gemini-3.1-flash-lite-preview", - name: "Gemini 3.1 Flash Lite Preview", + id: "gemini-3.1-flash-lite", + name: "Gemini 3.1 Flash Lite", toolCalling: true, supportsVision: true, }, @@ -72,18 +59,5 @@ export const geminiProvider: RegistryEntry = { toolCalling: true, supportsVision: true, }, - { - id: "gemini-2.0-flash-thinking-exp-01-21", - name: "Gemini 2.0 Flash Thinking", - supportsReasoning: true, - }, - { - id: "gemini-2.0-pro-exp-02-05", - name: "Gemini 2.0 Pro Experimental", - toolCalling: true, - supportsVision: true, - }, - { id: "gemini-1.5-pro", name: "Gemini 1.5 Pro", toolCalling: true, supportsVision: true }, - { id: "gemini-1.5-flash", name: "Gemini 1.5 Flash", toolCalling: true, supportsVision: true }, ], }; diff --git a/open-sse/config/providers/registry/gemini/web/index.ts b/open-sse/config/providers/registry/gemini/web/index.ts index 51f2b680dc..9b4ebe7021 100644 --- a/open-sse/config/providers/registry/gemini/web/index.ts +++ b/open-sse/config/providers/registry/gemini/web/index.ts @@ -13,7 +13,5 @@ export const gemini_webProvider: RegistryEntry = { { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" }, { id: "gemini-2.0-pro", name: "Gemini 2.0 Pro" }, { id: "gemini-2.0-flash", name: "Gemini 2.0 Flash" }, - { id: "gemini-1.5-pro", name: "Gemini 1.5 Pro" }, - { id: "gemini-1.5-flash", name: "Gemini 1.5 Flash" }, ], }; diff --git a/open-sse/config/rerankRegistry.ts b/open-sse/config/rerankRegistry.ts index bf14c0cc23..e44efb16e1 100644 --- a/open-sse/config/rerankRegistry.ts +++ b/open-sse/config/rerankRegistry.ts @@ -71,6 +71,39 @@ export const RERANK_PROVIDERS = { { id: "jina-reranker-m0", name: "Jina Reranker m0" }, ], }, + + // SiliconFlow rerank is Cohere-compatible (POST /v1/rerank, {model,query,documents}). The + // reranker models arrive in /v1/models via live model-sync; without this entry the rerank + // router rejected them with "Invalid rerank model" (#5332). Model IDs keep their vendor slash + // (e.g. "Qwen/Qwen3-Reranker-8B") — parseRerankModel splits on the FIRST slash, so it's safe. + siliconflow: { + id: "siliconflow", + baseUrl: "https://api.siliconflow.com/v1/rerank", + authType: "apikey", + authHeader: "bearer", + models: [ + { id: "Qwen/Qwen3-Reranker-8B", name: "Qwen3 Reranker 8B" }, + { id: "Qwen/Qwen3-Reranker-4B", name: "Qwen3 Reranker 4B" }, + { id: "Qwen/Qwen3-Reranker-0.6B", name: "Qwen3 Reranker 0.6B" }, + { id: "BAAI/bge-reranker-v2-m3", name: "BGE Reranker v2 m3" }, + ], + }, + + // DeepInfra rerank is NOT Cohere-shaped: POST /v1/inference/ with {queries:[q],documents} + // returning {scores:[…]} (one score per document, positional). The `deepinfra` format adapter in + // open-sse/handlers/rerank.ts builds the per-model URL and maps scores → Cohere results (#5332). + deepinfra: { + id: "deepinfra", + baseUrl: "https://api.deepinfra.com/v1/inference", + authType: "apikey", + authHeader: "bearer", + format: "deepinfra", + models: [ + { id: "Qwen/Qwen3-Reranker-8B", name: "Qwen3 Reranker 8B" }, + { id: "Qwen/Qwen3-Reranker-4B", name: "Qwen3 Reranker 4B" }, + { id: "Qwen/Qwen3-Reranker-0.6B", name: "Qwen3 Reranker 0.6B" }, + ], + }, }; const RERANK_PROVIDER_ALIASES = { diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index a0c8265feb..6795546ad4 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -7,6 +7,7 @@ import { applyContextEditingToBody } from "../config/contextEditing.ts"; import { findOffendingField, stripGroqUnsupportedFields } from "../config/providerFieldStrips.ts"; import { applyFingerprint, isCliCompatEnabled } from "../config/cliFingerprints.ts"; import { supportsClaudeMaxEffort, supportsXHighEffort } from "../config/providerModels.ts"; +import { getThinkingBudgetConfig, ThinkingMode } from "../services/thinkingBudget.ts"; import type { PoolConfig } from "../services/sessionPool/types.ts"; import type { Session } from "../services/sessionPool/session.ts"; import { SessionPool } from "../services/sessionPool/sessionPool.ts"; @@ -1059,12 +1060,21 @@ export class BaseExecutor { } else if (!effThinking && !headerEffort) { // Default CC logic when no override headers are present const isHaiku = typeof tb.model === "string" && tb.model.includes("haiku"); + // #5312 RC-B: honor the operator's proxy-level Thinking-Budget mode. + // `auto` means "strip — let the provider decide", so suppress the default + // adaptive injection. Passthrough/no-config keeps the native Claude Code + // behavior (adaptive) so #4633 does not regress (request-side only). + const tbMode = getThinkingBudgetConfig().mode; if (isHaiku) { // Keep tb.thinking — real Claude Desktop keeps thinking enabled for Haiku // (issue #2454). Only strip output_config (effort) which Haiku rejects; // context_management is re-paired with the preserved thinking below. delete tb.output_config; delete tb.context_management; + } else if (tbMode === ThinkingMode.AUTO) { + delete tb.thinking; + delete tb.context_management; + delete tb.output_config; } else if (tb.thinking === undefined && tb.output_config === undefined) { tb.thinking = { type: "adaptive" }; tb.context_management = { @@ -1072,6 +1082,18 @@ export class BaseExecutor { }; tb.output_config = { effort: "high" }; } + // #5312: Opus 4.7/4.8 accept only thinking.type="adaptive" ("enabled" → 400). + // When an operator budget (custom/adaptive mode) produced an enabled block + // upstream, remap it to adaptive + output_config.effort here. + const th = tb.thinking as Record | undefined; + if (th?.type === "enabled" && tbMode !== ThinkingMode.PASSTHROUGH) { + const b = typeof th.budget_tokens === "number" ? th.budget_tokens : 0; + tb.thinking = { type: "adaptive" }; + tb.output_config = { + effort: b <= 1024 ? "low" : b <= 10240 ? "medium" : b >= 131072 ? "max" : "high", + }; + tb.context_management = { edits: [{ type: "clear_thinking_20251015", keep: "all" }] }; + } } // Real CLI always pairs context_management with thinking. Mirror diff --git a/open-sse/executors/chatgpt-web.ts b/open-sse/executors/chatgpt-web.ts index 2ee0a7222d..7760d444c1 100644 --- a/open-sse/executors/chatgpt-web.ts +++ b/open-sse/executors/chatgpt-web.ts @@ -17,6 +17,8 @@ import { BaseExecutor, type ExecuteInput, type ProviderCredentials } from "./base.ts"; import { describeChatGptWebHttpError } from "./chatgptWebErrors.ts"; +import { prepareToolMessages } from "../translator/webTools.ts"; +import { buildToolModeResponse } from "./chatgptWebTools.ts"; import { createHash, randomUUID, randomBytes } from "node:crypto"; import { tlsFetchChatGpt, @@ -1560,28 +1562,15 @@ function buildStreamingResponse( } } - // If the assistant kicked off the async image_gen tool, the SSE - // stream ends with a "Processing image..." placeholder. Poll the - // conversation endpoint in the background for the final pointer. - // We only kick polling off if the in-stream pointers are empty — - // sometimes the synchronous path also fires and we already have one. - // Heartbeat helper: while we wait on long-running async work - // (WebSocket for image-gen, /files/download → 2-3 MB image fetch), - // the SSE stream goes quiet and Open WebUI's HTTP client times out - // at ~30s. We saw this in production: `disconnect: ResponseAborted` - // followed by "Controller is already closed". - // - // Layered traps to avoid: - // - SSE comments (`: ...`) are silently ignored by aiohttp's - // read-activity tracker. - // - Empty `delta:{}` chunks ARE emitted by us but get filtered - // out upstream by `hasValuableContent` in - // `open-sse/utils/streamHelpers.ts` (it requires content, - // role, or finish_reason on OpenAI chunks). - // - // So heartbeats are zero-width-space content deltas (`"​"`): - // they pass the valuable-content filter (non-empty content), reach - // the client as data events, and render as nothing visible. + // Async image_gen ends the SSE with a "Processing image..." + // placeholder; poll the conversation endpoint in the background for + // the final pointer (only when in-stream pointers are empty). + // Heartbeat: long async work (WebSocket image-gen, 2-3 MB image + // fetch) leaves the SSE quiet and Open WebUI times out at ~30s + // (`disconnect: ResponseAborted`). SSE comments and empty `delta:{}` + // chunks are both filtered upstream (`hasValuableContent` in + // open-sse/utils/streamHelpers.ts), so heartbeats are zero-width-space + // content deltas (`"​"`): they pass the filter and render invisibly. const startHeartbeat = (intervalMs = 5_000): (() => void) => { const heartbeatChunk = sseChunk({ id: cid, @@ -2467,6 +2456,13 @@ export class ChatGptWebExecutor extends BaseExecutor { }; } + // Tool-call emulation (#5240): inject a `` contract when `tools` are + // present; parsed back on the response side. Mirrors qwen-web/perplexity-web. + const { hasTools, requestedTools, effectiveMessages } = prepareToolMessages( + (body || {}) as Record, + messages as Array<{ role: string; content: unknown }> + ); + if (!credentials.apiKey) { return { response: errorResponse( @@ -2654,7 +2650,7 @@ export class ChatGptWebExecutor extends BaseExecutor { } // 4. Build conversation request - const parsed = parseOpenAIMessages(messages); + const parsed = parseOpenAIMessages(effectiveMessages); if (!parsed.currentMsg.trim() && parsed.history.length === 0) { return { response: errorResponse(400, "Empty user message"), @@ -2790,8 +2786,11 @@ export class ChatGptWebExecutor extends BaseExecutor { const pollAsyncImage = (conversationId: string) => pollForAsyncImage(conversationId, resolverCtx); + // Tool mode buffers (no live streaming) and is gated off the image-gen path. + const toolMode = hasTools && !forImageGen; + let finalResponse: Response; - if (stream) { + if (stream && !toolMode) { const sseStream = buildStreamingResponse( bodyStream, model, @@ -2822,6 +2821,13 @@ export class ChatGptWebExecutor extends BaseExecutor { log, signal ); + if (toolMode) { + finalResponse = await buildToolModeResponse(finalResponse, requestedTools, stream, { + cid, + created, + model, + }); + } } return { response: finalResponse, url: CONV_URL, headers, transformedBody: cgptBody }; diff --git a/open-sse/executors/chatgptWebTools.ts b/open-sse/executors/chatgptWebTools.ts new file mode 100644 index 0000000000..4a52506b33 --- /dev/null +++ b/open-sse/executors/chatgptWebTools.ts @@ -0,0 +1,119 @@ +// Tool-call emulation helpers for the ChatGPT Web executor (#5240). +// +// chatgpt.com has no native function calling. When the OpenAI request carries +// `tools`, the prompt-side shim (`prepareToolMessages` in +// ../translator/webTools.ts) injects a `` contract; on the response side +// we parse `{...}` blocks back into OpenAI `tool_calls` — +// mirroring the sibling web-session executors (qwen-web, perplexity-web, ...). +// +// The whole tool-mode orchestration lives here so the (frozen) chatgpt-web.ts +// only gains an import + a single delegating call. + +import { buildToolAwareResult } from "../translator/webTools.ts"; + +const SSE_HEADERS: Record = { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no", +}; + +function sseChunk(data: unknown): string { + return `data: ${JSON.stringify(data)}\n\n`; +} + +/** + * Parse any `` blocks in a buffered JSON completion's assistant content + * into OpenAI tool_calls and rewrite the choice. On parse failure the original + * body passes through untouched. + */ +async function applyToolCallsToJsonResponse( + response: Response, + requestedTools: unknown +): Promise { + const bodyText = await response.text(); + try { + const json = JSON.parse(bodyText); + const rawContent = json?.choices?.[0]?.message?.content || ""; + const { content, toolCalls, finishReason } = buildToolAwareResult( + rawContent, + requestedTools, + "cgpt" + ); + if (toolCalls) { + json.choices[0].message = { role: "assistant", content: null, tool_calls: toolCalls }; + json.choices[0].finish_reason = finishReason; + } else { + json.choices[0].message.content = content; + } + return new Response(JSON.stringify(json), { + status: response.status, + headers: { "Content-Type": "application/json" }, + }); + } catch { + return new Response(bodyText, { + status: response.status, + headers: { "Content-Type": "application/json" }, + }); + } +} + +/** + * Replay an already-built OpenAI `chat.completion` object as a buffered SSE + * stream: a role chunk, then a single terminal chunk carrying either + * `delta.tool_calls` + `finish_reason: "tool_calls"` or plain content + + * `finish_reason: "stop"`. No token-by-token streaming while tools are active. + */ +function toolCompletionToSseStream( + completion: Record, + cid: string, + created: number, + model: string +): ReadableStream { + const encoder = new TextEncoder(); + const choice = (completion?.choices as Array> | undefined)?.[0] ?? {}; + const message = (choice.message as Record) ?? {}; + const finishReason = (choice.finish_reason as string) ?? "stop"; + const chunk = (delta: Record, fr: string | null): Uint8Array => + encoder.encode( + sseChunk({ + id: cid, + object: "chat.completion.chunk", + created, + model, + system_fingerprint: null, + choices: [{ index: 0, delta, finish_reason: fr, logprobs: null }], + }) + ); + + return new ReadableStream({ + start(controller) { + controller.enqueue(chunk({ role: "assistant" }, null)); + const delta = message.tool_calls + ? { tool_calls: message.tool_calls } + : { content: (message.content as string) ?? "" }; + controller.enqueue(chunk(delta, finishReason)); + controller.enqueue(encoder.encode("data: [DONE]\n\n")); + controller.close(); + }, + }); +} + +/** + * Tool mode: parse `` blocks in an already-buffered JSON completion into + * tool_calls, then return either the JSON completion (non-streaming) or a + * terminal SSE replay of it (streaming). + */ +export async function buildToolModeResponse( + bufferedJson: Response, + requestedTools: unknown, + stream: boolean, + meta: { cid: string; created: number; model: string } +): Promise { + const jsonResponse = await applyToolCallsToJsonResponse(bufferedJson, requestedTools); + if (!stream) return jsonResponse; + const completion = await jsonResponse.json(); + return new Response(toolCompletionToSseStream(completion, meta.cid, meta.created, meta.model), { + status: 200, + headers: SSE_HEADERS, + }); +} diff --git a/open-sse/executors/opencode.ts b/open-sse/executors/opencode.ts index ce68ef694c..6fccf7117d 100644 --- a/open-sse/executors/opencode.ts +++ b/open-sse/executors/opencode.ts @@ -152,7 +152,11 @@ export class OpencodeExecutor extends BaseExecutor { for (let attempt = 0; attempt < this.accounts.length; attempt++) { const account = this.pickAccount(); const masked = OpencodeExecutor.maskAccountId(account.fingerprint); - log?.debug?.( + // #5217 (Gap 2): promoted debug→info so the per-request account/proxy + // rotation selection is visible in the Console log view at the default + // APP_LOG_LEVEL=info (users could not see which account/proxy was used). + // Token stays masked — never log the full account id. + log?.info?.( "OPENCODE", `dispatch via account ${masked} (idx ${attempt + 1}/${this.accounts.length})` + (account.proxy ? ` through proxy ${account.proxy.host}:${account.proxy.port}` : " direct") diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 0d22e66837..fb371aa1cb 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -73,6 +73,10 @@ import { withBodyTimeout, } from "../utils/stream.ts"; import { ensureStreamReadiness } from "../utils/streamReadiness.ts"; +import { + resolveSuppressThinkClose, + THINKING_MARKER_HEADER, +} from "../utils/thinkCloseMarker.ts"; import { resolveStreamReadinessTimeout } from "../utils/streamReadinessPolicy.ts"; import { createStreamController } from "../utils/streamHandler.ts"; import * as streamFailure from "../utils/streamFailureFinalization.ts"; @@ -787,6 +791,15 @@ export async function handleChatCore({ .filter(Boolean) .join(" "); + // Explicit per-request opt-in/out for the `` close marker + // (#5312 / #5245): `x-omniroute-thinking-marker: off` suppresses it for + // reasoning_content-native clients (e.g. Cursor's OpenAI path) that the UA + // allowlist does not cover; absent the header, the UA policy applies. + const thinkingMarkerHeader = getHeaderValueCaseInsensitive( + clientRawRequest?.headers ?? null, + THINKING_MARKER_HEADER + ); + const explicitStreamAlias = resolveExplicitStreamAlias(body); // Remove non-standard non-stream aliases before provider translation/execution. @@ -4188,7 +4201,15 @@ export async function handleChatCore({ onStreamComplete, apiKeyInfo, handleStreamFailure, - copilotCompatibleReasoning + copilotCompatibleReasoning, + // Suppress the `` close marker for clients that render it verbatim + // (e.g. OpenCode by UA; any client via `x-omniroute-thinking-marker: off`); + // preserved for Claude Code / Cursor and unknown clients by default (#5245 / + // #5312). The header wins over the UA allowlist. + resolveSuppressThinkClose({ + userAgent: streamUserAgent, + thinkingMarkerHeader, + }) ); } else { log?.debug?.("STREAM", `Standard passthrough mode`); diff --git a/open-sse/handlers/chatCore/keyHealth.ts b/open-sse/handlers/chatCore/keyHealth.ts index 91e7c63618..100185b442 100644 --- a/open-sse/handlers/chatCore/keyHealth.ts +++ b/open-sse/handlers/chatCore/keyHealth.ts @@ -7,6 +7,8 @@ * (apiKeyRotator) for the connection's currently-selected key, and persists the change to the * provider connection so it survives process restarts: * - 401 → record a failure (warning, then invalid at the threshold), always persisted. + * - 402 → terminal (insufficient balance); mark the current key invalid immediately (#5239), + * persisted on the active→invalid transition. * - 2xx → record a success, persisted only when recovering from a warning/invalid state. * Any other status only refreshes the tracked extra-key set. The handler binds its `log` once and * delegates here, keeping the existing call sites unchanged. @@ -15,6 +17,7 @@ import { recordKeyFailure, recordKeySuccess, + recordKeyTerminal, trackConnectionExtraKeys, type KeyHealth, } from "../../services/apiKeyRotator.ts"; @@ -64,6 +67,32 @@ export function recordKeyHealthStatus( ); }); } + } else if (status === 402) { + // 402 "Insufficient account balance" is terminal for this key — the balance + // won't recover mid-session, so mark the current key invalid immediately + // (don't wait for FAILURE_THRESHOLD) so the rotator stops returning it. + // The per-connection path already terminalizes 402 via credits_exhausted; + // this closes the per-KEY gap (#5239) for API Key Round-Robin connections. + const updatedHealth = recordKeyTerminal(connId, currentKeyId); + log?.error?.( + "AUTH", + `402 on connection ${connId.slice(0, 8)} - key ${currentKeyId} marked invalid (insufficient balance)` + ); + + const prevStatus = health?.[currentKeyId]?.status; + if (updatedHealth.status !== prevStatus) { + updateProviderConnection(connId, { + providerSpecificData: { + ...psd, + apiKeyHealth: { ...health, [currentKeyId]: updatedHealth }, + }, + }).catch((err: unknown) => { + log?.error?.( + "DB", + `Failed to persist apiKeyHealth: ${err instanceof Error ? err.message : String(err)}` + ); + }); + } } else if (status >= 200 && status < 300) { const updatedHealth = recordKeySuccess(connId, currentKeyId); const prevStatus = health?.[currentKeyId]?.status; diff --git a/open-sse/handlers/rerank.ts b/open-sse/handlers/rerank.ts index b58c38d820..6200a168ac 100644 --- a/open-sse/handlers/rerank.ts +++ b/open-sse/handlers/rerank.ts @@ -26,7 +26,7 @@ function buildAuthHeader(providerConfig, token) { /** * Transform request body for provider-specific formats (e.g. NVIDIA ranking API) */ -function transformRequestForProvider(providerConfig, body) { +/* @testonly */ export function transformRequestForProvider(providerConfig, body) { if (providerConfig.format === "nvidia") { return { model: body.model, @@ -37,14 +37,24 @@ function transformRequestForProvider(providerConfig, body) { top_n: body.top_n, }; } - // Default: Cohere-compatible format (used by Together, Fireworks, Cohere) + // DeepInfra inference API: the model goes in the URL path (handled by the caller), the body + // carries {queries:[query], documents:[strings]} and the response is a positional {scores:[…]}. + if (providerConfig.format === "deepinfra") { + return { + queries: [body.query], + documents: (body.documents || []).map((doc) => + typeof doc === "string" ? doc : doc.text || "" + ), + }; + } + // Default: Cohere-compatible format (used by Together, Fireworks, Cohere, SiliconFlow) return body; } /** * Transform response from provider-specific formats back to Cohere format */ -/* @testonly */ export function transformResponseFromProvider(providerConfig, data) { +/* @testonly */ export function transformResponseFromProvider(providerConfig, data, options = {}) { if (providerConfig.format === "nvidia") { return { id: data.id != null ? String(data.id) : `rerank-${Date.now()}`, @@ -59,6 +69,31 @@ function transformRequestForProvider(providerConfig, body) { }, }; } + // DeepInfra returns {scores:[…]} — one float per document, in document order. Map to Cohere's + // results[] (index + relevance_score + optional document), sorted by score desc, honoring top_n. + if (providerConfig.format === "deepinfra") { + const documents = Array.isArray(options.documents) ? options.documents : []; + const returnDocuments = options.return_documents !== false; + const scored = (Array.isArray(data.scores) ? data.scores : []).map((score, index) => { + const doc = documents[index]; + const text = typeof doc === "string" ? doc : doc?.text || ""; + return { + index, + relevance_score: typeof score === "number" ? score : 0, + ...(returnDocuments ? { document: { text } } : {}), + }; + }); + scored.sort((a, b) => b.relevance_score - a.relevance_score); + const topN = typeof options.top_n === "number" && options.top_n > 0 ? options.top_n : undefined; + return { + id: `rerank-${Date.now()}`, + results: topN ? scored.slice(0, topN) : scored, + meta: { + api_version: { version: "2" }, + billed_units: { search_units: 1 }, + }, + }; + } return data; } @@ -114,8 +149,15 @@ export async function handleRerank({ return_documents: return_documents !== false, }); + // DeepInfra puts the model in the URL path (POST /v1/inference/); all others use a fixed + // rerank endpoint with the model in the body. + const rerankUrl = + providerConfig.format === "deepinfra" + ? `${providerConfig.baseUrl}/${modelId}` + : providerConfig.baseUrl; + try { - const res = await fetch(providerConfig.baseUrl, { + const res = await fetch(rerankUrl, { method: "POST", headers: { "Content-Type": "application/json", @@ -133,7 +175,11 @@ export async function handleRerank({ } const data = await res.json(); - const result = transformResponseFromProvider(providerConfig, data); + const result = transformResponseFromProvider(providerConfig, data, { + documents, + top_n: top_n || documents.length, + return_documents, + }); const searchUnits = Number(result?.meta?.billed_units?.search_units) || 0; const costUsd = await calculateModalCost("rerank", providerId, modelId, { searchUnits }); diff --git a/open-sse/package.json b/open-sse/package.json index bfc331719e..e414d4071a 100644 --- a/open-sse/package.json +++ b/open-sse/package.json @@ -1,6 +1,6 @@ { "name": "@omniroute/open-sse", - "version": "3.8.40", + "version": "3.8.41", "description": "Express SSE sidecar for OmniRoute — handles streaming, protocol translation, and provider orchestration", "type": "module", "main": "index.js", diff --git a/open-sse/services/accountFallback.ts b/open-sse/services/accountFallback.ts index d3bf559519..4139845c2e 100644 --- a/open-sse/services/accountFallback.ts +++ b/open-sse/services/accountFallback.ts @@ -159,6 +159,12 @@ export const CREDITS_EXHAUSTED_SIGNALS = [ "out of credits", "payment required", "free tier of the model has been exhausted", + // #5239: providers (e.g. DeepSeek/GLM-style) return "Insufficient account balance" + // on a depleted key. 402 is already terminalized by status, but catch non-402 + // out-of-credit bodies here too. + "insufficient balance", + "insufficient_balance", + "insufficient account balance", ]; // T11: Signals that indicate OAuth token is invalid/expired (not permanent deactivation) diff --git a/open-sse/services/apiKeyRotator.ts b/open-sse/services/apiKeyRotator.ts index 58a1337f0d..f63972b8dc 100644 --- a/open-sse/services/apiKeyRotator.ts +++ b/open-sse/services/apiKeyRotator.ts @@ -189,6 +189,29 @@ export function recordKeyFailure(connectionId: string, keyId: string): KeyHealth return { ...health }; } +/** + * Record a terminal failure for a key — e.g. HTTP 402 "Insufficient account + * balance". Unlike recordKeyFailure() (which only invalidates after + * FAILURE_THRESHOLD consecutive failures), this marks the key "invalid" + * immediately in a single call, because the condition will not recover + * mid-session (the depleted key must not be returned by the rotator again + * until credits are added / an operator resets it). + * + * @param connectionId - Connection scope for health state isolation + * @param keyId - Key identifier ("primary" | "extra_0" | ...) + * @returns Updated health status + */ +export function recordKeyTerminal(connectionId: string, keyId: string): KeyHealth { + const health = getOrCreateHealth(connectionId, keyId); + health.failures = Math.max(health.failures + 1, FAILURE_THRESHOLD); + health.totalRequests++; + health.totalFailures++; + health.lastFailure = new Date().toISOString(); + health.status = "invalid"; + + return { ...health }; +} + /** * Record a successful authentication attempt for a key. * Resets failure count and marks as "active". diff --git a/open-sse/services/modelDeprecation.ts b/open-sse/services/modelDeprecation.ts index 25625157cb..b38b76c36f 100644 --- a/open-sse/services/modelDeprecation.ts +++ b/open-sse/services/modelDeprecation.ts @@ -18,8 +18,12 @@ const BUILT_IN_ALIASES: Record = { "gemini-1.5-flash": "gemini-2.5-flash", "gemini-1.0-pro": "gemini-2.5-pro", "gemini-2.0-flash": "gemini-2.5-flash", + "gemini-2.0-flash-lite": "gemini-3.1-flash-lite", + "gemini-3.1-flash-lite-preview": "gemini-3.1-flash-lite", "gemini-3-pro-high": "gemini-3.1-pro-high", "gemini-3-pro-low": "gemini-3.1-pro-low", + // Retired free Gemma (was in the gemini-free pool) → current gemini-free model + "gemma-4": "gemini-3.1-flash-lite", // Claude legacy → current "claude-3-opus-20240229": "claude-opus-4-20250514", diff --git a/open-sse/services/thinkingBudget.ts b/open-sse/services/thinkingBudget.ts index 066698334b..a16f3b432d 100644 --- a/open-sse/services/thinkingBudget.ts +++ b/open-sse/services/thinkingBudget.ts @@ -82,6 +82,23 @@ export function getThinkingBudgetConfig() { return { ..._config }; } +/** + * Startup hydration (#5312 RC-A): the dashboard Thinking-Budget setting is persisted + * under `settings.thinkingBudget`, but nothing read it back at boot, so `_config` + * reset to DEFAULT (passthrough) on every restart. Call this once during server + * bootstrap with the loaded settings object to restore the operator's choice. + * Returns true when a valid config was applied, false otherwise (zero behavior + * change when the setting is unset). + */ +export function hydrateThinkingBudgetConfig(settings: unknown): boolean { + const tb = toRecord(settings).thinkingBudget; + if (tb && typeof tb === "object" && !Array.isArray(tb)) { + setThinkingBudgetConfig(tb as Partial); + return true; + } + return false; +} + /** * Normalize thinkingLevel string fields into numeric budget. * Handles: body.thinkingLevel, body.thinking_level, diff --git a/open-sse/translator/request/openai-to-claude.ts b/open-sse/translator/request/openai-to-claude.ts index c4cfae7a3a..2180bef00e 100644 --- a/open-sse/translator/request/openai-to-claude.ts +++ b/open-sse/translator/request/openai-to-claude.ts @@ -557,6 +557,17 @@ export function openaiToClaudeRequest(model, body, stream) { result._toolNameMap = toolNameMap; } + // Empty-messages guard. Claude's Messages API rejects an empty `messages` + // array with `400 messages: at least one message is required`. This happens + // when the incoming OpenAI request carried only `system`/`developer` turns + // (e.g. an all-system compaction or title-generation request from a client + // like OpenCode): those are hoisted into `result.system` above, leaving + // `messages` empty. Synthesize a minimal user turn so the request stays + // valid — the system instructions still drive the response. (#5245) + if (result.messages.length === 0) { + result.messages.push({ role: "user", content: [{ type: "text", text: "." }] }); + } + return result; } @@ -628,12 +639,19 @@ function getContentBlocksFromMessage(msg, toolNameMap = new Map(), disableToolPr } } } else if (msg.role === "assistant") { - // Add reasoning_content as thinking block (OpenAI extended thinking format) + // Add reasoning_content as a replay placeholder (OpenAI extended thinking format). + // #5312 RC-D: reasoning_content carries NO real Claude signature. Emitting a + // `thinking` block with the fabricated DEFAULT signature makes Anthropic reject the + // replay with 400 "Invalid signature in thinking block" — and claudeHelper's + // latest-assistant guard (prepareClaudeRequest) preserves it verbatim, so the fake + // signature leaks upstream. Emit a signature-less redacted_thinking block instead + // (the same shape prepareClaudeRequest produces for Anthropic-native replay); + // Anthropic accepts it without signature validation and non-Anthropic Claude-shape + // upstreams re-hydrate the real text downstream from reasoningCache. if (msg.reasoning_content) { blocks.push({ - type: "thinking", - thinking: msg.reasoning_content, - signature: DEFAULT_THINKING_CLAUDE_SIGNATURE, + type: "redacted_thinking", + data: DEFAULT_THINKING_CLAUDE_SIGNATURE, }); } diff --git a/open-sse/translator/response/claude-to-openai.ts b/open-sse/translator/response/claude-to-openai.ts index d6340e1423..725799d026 100644 --- a/open-sse/translator/response/claude-to-openai.ts +++ b/open-sse/translator/response/claude-to-openai.ts @@ -81,7 +81,11 @@ export function claudeToOpenAIResponse(chunk, state) { // immediately before the assistant reply begins — but NOT in tool-use streams // where no text_delta ever arrives (#5123). if (state.pendingThinkClose) { - results.push(createChunk(state, { content: "" })); + // Suppressed for clients that render the marker verbatim (#5245); + // still clear the flag so it never re-fires later in the stream. + if (!state.suppressThinkClose) { + results.push(createChunk(state, { content: "" })); + } state.pendingThinkClose = false; } results.push(createChunk(state, { content: delta.text })); @@ -195,7 +199,10 @@ export function claudeToOpenAIResponse(chunk, state) { // responses that had no text_delta (edge case: thinking-only with // immediate stop) still receive the marker here. if (state.pendingThinkClose && state.toolCalls.size === 0) { - results.push(createChunk(state, { content: "" })); + // Suppressed for clients that render the marker verbatim (#5245). + if (!state.suppressThinkClose) { + results.push(createChunk(state, { content: "" })); + } state.pendingThinkClose = false; } state.finishReason = convertStopReason(chunk.delta.stop_reason); diff --git a/open-sse/utils/proxyFetch.ts b/open-sse/utils/proxyFetch.ts index 680377009d..134aaf8311 100644 --- a/open-sse/utils/proxyFetch.ts +++ b/open-sse/utils/proxyFetch.ts @@ -28,6 +28,31 @@ function isTlsFingerprintEnabled() { type TlsFingerprintStore = { used: boolean }; const tlsFingerprintContext = new AsyncLocalStorage(); +/** + * #5217 (Gap-secondary): a mutable sink that records the proxy actually applied + * by `runWithProxyContext` for the in-flight request. Executors that pin their + * own per-account proxy *internally* (e.g. OpencodeExecutor wraps its dispatch + * in `runWithProxyContext(account.proxy, …)`) never propagate that choice back + * to the caller's `proxyInfo`, so the post-execution `[ProxyEgress]` line logged + * `proxy=direct` even though `[ProxyFetch] Applied request proxy context: …` + * fired. Wrapping the execution in `runWithAppliedProxyCapture(sink, fn)` lets + * the egress logger read the innermost applied proxy (the last writer wins, which + * is the executor's per-account proxy). + */ +export type AppliedProxySink = { proxy: unknown }; +const appliedProxyContext = new AsyncLocalStorage(); + +/** + * Run `fn` with an applied-proxy capture sink in context. Any + * `runWithProxyContext` call inside `fn` that ends up applying a proxy records + * that proxy config into `sink.proxy` (innermost wins). The sink is a plain + * mutable object the caller retains, so it can read `sink.proxy` after `fn` + * resolves. Pure plumbing — no behavioral change to the request itself. + */ +export function runWithAppliedProxyCapture(sink: AppliedProxySink, fn: () => T): T { + return appliedProxyContext.run(sink, fn); +} + type FetchWithDispatcherOptions = RequestInit & { dispatcher?: unknown }; type FetchWithDispatcher = ( input: RequestInfo | URL, @@ -334,6 +359,14 @@ export async function runWithProxyContext( `[ProxyFetch] Applied request proxy context: ${proxyUrlForLogs(resolvedProxyUrl)}` ); } + // #5217: record the proxy actually applied so a post-execution egress logger + // reflects the real egress (executors that pin a per-account proxy internally + // otherwise leave proxyInfo reading "direct"). Innermost runWithProxyContext + // wins, which is exactly the per-account proxy the executor selected. + if (effectiveProxyConfig) { + const sink = appliedProxyContext.getStore(); + if (sink) sink.proxy = effectiveProxyConfig; + } return fn(); }); } diff --git a/open-sse/utils/stream.ts b/open-sse/utils/stream.ts index 046875666a..b6feb86e0a 100644 --- a/open-sse/utils/stream.ts +++ b/open-sse/utils/stream.ts @@ -117,6 +117,8 @@ type StreamOptions = { sourceFormat?: string; clientResponseFormat?: string | null; copilotCompatibleReasoning?: boolean; + /** Suppress the `` close marker for clients that render it verbatim (#5245). */ + suppressThinkClose?: boolean; provider?: string | null; reqLogger?: StreamLogger | null; toolNameMap?: unknown; @@ -135,6 +137,8 @@ type TranslateState = ReturnType & { usage?: unknown; finishReason?: unknown; copilotCompatibleReasoning?: boolean; + /** Suppress the `` close marker for clients that render it verbatim (#5245). */ + suppressThinkClose?: boolean; /** Accumulated message content for call log response body */ accumulatedContent?: string; upstreamError?: { @@ -607,6 +611,7 @@ export function createSSEStream(options: StreamOptions = {}) { sourceFormat, clientResponseFormat = null, copilotCompatibleReasoning = false, + suppressThinkClose = false, provider = null, reqLogger = null, toolNameMap = null, @@ -660,6 +665,7 @@ export function createSSEStream(options: StreamOptions = {}) { toolNameMap, signatureNamespace, copilotCompatibleReasoning, + suppressThinkClose, accumulatedContent: "", } : null; @@ -2636,7 +2642,8 @@ export function createSSETransformStreamWithLogger( onComplete: ((payload: StreamCompletePayload) => void) | null = null, apiKeyInfo: unknown = null, onFailure: ((payload: StreamFailurePayload) => void | Promise) | null = null, - copilotCompatibleReasoning = false + copilotCompatibleReasoning = false, + suppressThinkClose = false ) { return createSSEStream({ mode: STREAM_MODE.TRANSLATE, @@ -2652,6 +2659,7 @@ export function createSSETransformStreamWithLogger( onComplete, onFailure, copilotCompatibleReasoning, + suppressThinkClose, }); } diff --git a/open-sse/utils/thinkCloseMarker.ts b/open-sse/utils/thinkCloseMarker.ts new file mode 100644 index 0000000000..bea9a75c49 --- /dev/null +++ b/open-sse/utils/thinkCloseMarker.ts @@ -0,0 +1,73 @@ +/** + * `` close-marker client policy. + * + * When OmniRoute translates a Claude-native streamed response to OpenAI Chat + * Completions shape (`claude-to-openai.ts`), it emits a single `` + * close marker as `delta.content` so clients that scan content for the marker + * (Claude Code, Cursor) can split reasoning from the final answer — see #4633. + * + * Some OpenAI-compatible consumers do NOT parse that marker and render it + * verbatim, so a bare `` leaks into the visible reply (#5245). OpenCode + * is one such client. + * + * Policy is conservative and opt-OUT by allowlist: the marker stays ON by + * default (preserving #4633 for Claude Code / Cursor and any unrecognized + * client), and is suppressed ONLY for known clients that render it literally. + * Detection is by inbound `User-Agent`. + * + * Clients that DO render the marker verbatim but are not in the UA allowlist + * (e.g. Cursor's reasoning_content-native OpenAI path — #5312 / #5245) can opt + * in explicitly with the request header `x-omniroute-thinking-marker: off`, + * which suppresses the marker regardless of User-Agent. `on` forces it kept + * (overriding the UA allowlist). The default (header absent) is byte-identical + * to the UA-only policy, so #4633 / #5123 are never regressed. + */ + +/** Header clients send to explicitly opt in/out of the `` close marker. */ +export const THINKING_MARKER_HEADER = "x-omniroute-thinking-marker"; + +// Lowercased User-Agent substrings of clients that render the textual +// `` marker verbatim and therefore want it suppressed. +const SUPPRESS_THINK_CLOSE_UA_MARKERS = ["opencode"]; + +/** + * Whether the streamed `` close marker should be suppressed for the + * given inbound client. Returns false (emit the marker) for unknown clients and + * for Claude Code / Cursor, so #4633 is never regressed. + */ +export function shouldSuppressThinkCloseMarker(userAgent: string | null | undefined): boolean { + if (!userAgent || typeof userAgent !== "string") return false; + const ua = userAgent.toLowerCase(); + return SUPPRESS_THINK_CLOSE_UA_MARKERS.some((marker) => ua.includes(marker)); +} + +/** + * Interpret the explicit `x-omniroute-thinking-marker` request header. + * Returns `true` (suppress the marker), `false` (force-keep the marker), or + * `null` when the header is absent/unrecognized (defer to the UA policy). + */ +export function thinkingMarkerHeaderSignal( + headerValue: string | null | undefined +): boolean | null { + if (typeof headerValue !== "string") return null; + const value = headerValue.trim().toLowerCase(); + if (value === "off" || value === "false" || value === "0" || value === "suppress") return true; + if (value === "on" || value === "true" || value === "1" || value === "keep") return false; + return null; +} + +/** + * Resolve whether the streamed `` close marker should be suppressed for + * this request. An explicit `x-omniroute-thinking-marker` header wins; absent + * that, the conservative User-Agent allowlist policy applies. With no header and + * an unrecognized UA the result is `false` (marker kept), so #4633 / #5123 stay + * byte-identical by default. + */ +export function resolveSuppressThinkClose(opts: { + userAgent?: string | null; + thinkingMarkerHeader?: string | null; +}): boolean { + const headerSignal = thinkingMarkerHeaderSignal(opts.thinkingMarkerHeader); + if (headerSignal !== null) return headerSignal; + return shouldSuppressThinkCloseMarker(opts.userAgent); +} diff --git a/package-lock.json b/package-lock.json index aeae5186c1..e0d9c8f6f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute", - "version": "3.8.40", + "version": "3.8.41", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute", - "version": "3.8.40", + "version": "3.8.41", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -28545,7 +28545,7 @@ }, "open-sse": { "name": "@omniroute/open-sse", - "version": "3.8.40", + "version": "3.8.41", "dependencies": { "@toon-format/toon": "^2.3.0", "safe-regex": "^2.1.1" diff --git a/package.json b/package.json index 3bcc660390..bb879518b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniroute", - "version": "3.8.40", + "version": "3.8.41", "description": "Unified AI router with 160+ providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.", "type": "module", "bin": { @@ -29,6 +29,7 @@ "scripts/build/colocateOptionals.mjs", "scripts/build/sync-env.mjs", "scripts/dev/responses-ws-proxy.mjs", + "scripts/dev/tls-options.mjs", "scripts/check/check-supported-node-runtime.ts", "scripts/dev/sync-env.mjs", "scripts/build/native-binary-compat.mjs", diff --git a/scripts/build/assembleStandalone.mjs b/scripts/build/assembleStandalone.mjs index 7df83d1444..6ee88d58a7 100644 --- a/scripts/build/assembleStandalone.mjs +++ b/scripts/build/assembleStandalone.mjs @@ -149,6 +149,12 @@ const EXTRA_MODULE_ENTRIES = [ src: ["scripts", "dev", "webdav-handler.mjs"], dest: ["webdav-handler.mjs"], }, + { + // #5242: opt-in HTTPS/TLS resolver (server-ws.mjs dependency). + label: "tls-options (server-ws.mjs dependency)", + src: ["scripts", "dev", "tls-options.mjs"], + dest: ["tls-options.mjs"], + }, { label: "runtime-env script", src: ["scripts", "build", "runtime-env.mjs"], diff --git a/scripts/build/pack-artifact-policy.ts b/scripts/build/pack-artifact-policy.ts index 1cae4e6b03..ba0451059e 100644 --- a/scripts/build/pack-artifact-policy.ts +++ b/scripts/build/pack-artifact-policy.ts @@ -44,6 +44,7 @@ export const APP_STAGING_ALLOWED_EXACT_PATHS: string[] = [ "peer-stamp.mjs", "responses-ws-proxy.mjs", "scripts/dev/sync-env.mjs", + "scripts/dev/tls-options.mjs", "server.js", "server-ws.mjs", "webdav-handler.mjs", @@ -111,6 +112,9 @@ export const PACK_ARTIFACT_ROOT_ALLOWED_EXACT_PATHS: string[] = [ "scripts/build/sync-env.mjs", "scripts/dev/responses-ws-proxy.mjs", "scripts/dev/sync-env.mjs", + // #5361: imported at runtime by bin/cli/commands/serve.mjs + the standalone + // server wrapper for opt-in native HTTPS/TLS serving (kept dependency-light). + "scripts/dev/tls-options.mjs", "scripts/postinstall.mjs", "src/shared/utils/nodeRuntimeSupport.ts", ]; diff --git a/scripts/build/runtime-env.mjs b/scripts/build/runtime-env.mjs index 6423183756..e0306e31f3 100644 --- a/scripts/build/runtime-env.mjs +++ b/scripts/build/runtime-env.mjs @@ -36,6 +36,55 @@ export function calibrateHeapFallbackMb(totalmemBytes) { return Math.min(4096, Math.max(512, target)); } +const MAX_OLD_SPACE_FLAG = "--max-old-space-size"; + +/** + * True when the caller already pinned the V8 heap via NODE_OPTIONS + * (`--max-old-space-size=…`). Used to decide whether `omniroute serve` may + * append/inject the calibrated default — a user-set value must always win. + * @param {NodeJS.ProcessEnv | Record} [env] + */ +export function envHasExplicitHeapFlag(env = process.env) { + return String(env?.NODE_OPTIONS || "").includes(MAX_OLD_SPACE_FLAG); +} + +/** + * Assemble the NODE_OPTIONS string for the spawned server, preserving any flags + * the user already exported. #5238: `omniroute serve` used to UNCONDITIONALLY + * overwrite NODE_OPTIONS with the calibrated `--max-old-space-size`, silently + * discarding a user-set `NODE_OPTIONS=--max-old-space-size=8192` (reporter set + * 8192 and still OOM'd at ~505MB). Mirrors the Electron (electron/main.js) and + * standalone (scripts/dev/run-standalone.mjs) launchers: + * - if NODE_OPTIONS already contains `--max-old-space-size`, keep it as-is + * (the user's value wins); + * - otherwise append the calibrated `--max-old-space-size=` to + * the existing NODE_OPTIONS, preserving unrelated flags (e.g. + * `--enable-source-maps`). + * @param {NodeJS.ProcessEnv | Record} [env] + * @param {number} memoryLimit — calibrated V8 heap ceiling (MB) + * @returns {string} the NODE_OPTIONS value to pass to the child process + */ +export function buildServerNodeOptions(env = process.env, memoryLimit) { + const existing = String(env?.NODE_OPTIONS || "").trim(); + if (existing.includes(MAX_OLD_SPACE_FLAG)) return existing; + return `${existing} ${MAX_OLD_SPACE_FLAG}=${memoryLimit}`.trim(); +} + +/** + * Build the leading `node` CLI args that pin the V8 heap. When the user already + * pinned the heap via NODE_OPTIONS, return `[]` so we do NOT inject a + * conflicting/shadowing CLI `--max-old-space-size` (CLI args override + * NODE_OPTIONS, which would re-introduce #5238). Otherwise return the calibrated + * flag — NODE_OPTIONS already carries the same value, so this stays redundant + * (identical value), never conflicting. + * @param {NodeJS.ProcessEnv | Record} [env] + * @param {number} memoryLimit — calibrated V8 heap ceiling (MB) + * @returns {string[]} + */ +export function buildNodeHeapArgs(env = process.env, memoryLimit) { + return envHasExplicitHeapFlag(env) ? [] : [`${MAX_OLD_SPACE_FLAG}=${memoryLimit}`]; +} + /** * @param {NodeJS.ProcessEnv | Record} [fromEnv] * Defaults to process.env. Pass bootstrap `merged` so project `.env` PORT applies before spawn. diff --git a/scripts/dev/standalone-server-ws.mjs b/scripts/dev/standalone-server-ws.mjs index bb8941d56e..890caee40f 100644 --- a/scripts/dev/standalone-server-ws.mjs +++ b/scripts/dev/standalone-server-ws.mjs @@ -5,11 +5,23 @@ import { createResponsesWsProxy } from "./responses-ws-proxy.mjs"; import { ensurePeerStampToken, wrapRequestListenerWithPeerStamp } from "./peer-stamp.mjs"; import { maybeHandleWebdav } from "./webdav-handler.mjs"; import methodGuard from "./http-method-guard.cjs"; +import { resolveTlsOptions, createServerListener } from "./tls-options.mjs"; const originalCreateServer = http.createServer.bind(http); const proxiesByPort = new Map(); const { wrapRequestListenerWithMethodGuard } = methodGuard; +// Opt-in native HTTPS (#5242). Resolved once at boot: when both OMNIROUTE_TLS_CERT +// and OMNIROUTE_TLS_KEY point at readable files we terminate TLS on the same +// listener Next binds to (so WS `upgrade` / request wrappers keep working over +// TLS). Absent or misconfigured → null → identical plain-HTTP behavior as before. +const tlsOptions = resolveTlsOptions(process.env); +if (tlsOptions) { + console.log( + `[omniroute][tls] HTTPS enabled — terminating TLS with cert=${tlsOptions.certPath}` + ); +} + process.env.OMNIROUTE_WS_BRIDGE_SECRET ||= randomUUID(); // Per-process secret proving the trusted peer-IP stamp came from this server. ensurePeerStampToken(); @@ -107,7 +119,10 @@ http.createServer = function createServerWithResponsesWs(...args) { ); } - const server = originalCreateServer(...args); + // When TLS is configured, return an https.Server (terminating TLS on the same + // listener); otherwise the original http.Server. The downstream .on/.addListener + // patches below apply identically to both (https.Server extends http.Server). + const server = createServerListener(args, tlsOptions, { createHttp: originalCreateServer }); const originalOn = server.on.bind(server); const originalAddListener = server.addListener.bind(server); diff --git a/scripts/dev/tls-options.mjs b/scripts/dev/tls-options.mjs new file mode 100644 index 0000000000..5eb439e133 --- /dev/null +++ b/scripts/dev/tls-options.mjs @@ -0,0 +1,92 @@ +// scripts/dev/tls-options.mjs +// +// Pure, dependency-light helpers for OmniRoute's opt-in native HTTPS/TLS serving +// (#5242, Bug 1C). Kept side-effect-free and free of heavy imports so it can be +// imported both by the CLI (bin/cli/commands/serve.mjs) and by the standalone +// server wrapper (standalone-server-ws.mjs), and unit-tested in isolation. +// +// TLS is strictly opt-in: when neither cert nor key is provided the server +// behaves EXACTLY as before (plain HTTP). A misconfiguration (only one of the +// pair, or an unreadable path) NEVER crashes the server — it logs a warning and +// falls back to HTTP, preserving today's behavior and loopback/security posture. + +import fs from "node:fs"; +import http from "node:http"; +import https from "node:https"; + +/** + * Resolve TLS options from environment variables. + * + * Reads `OMNIROUTE_TLS_CERT` and `OMNIROUTE_TLS_KEY` (filesystem paths). Returns + * `{ cert, key }` (file contents) only when BOTH are provided and readable. + * Otherwise returns `null` so the caller serves plain HTTP — never throwing. + * + * @param {NodeJS.ProcessEnv} [env=process.env] + * @param {{ readFileSync?: typeof fs.readFileSync, warn?: (msg: string) => void }} [deps] + * @returns {{ cert: Buffer|string, key: Buffer|string, certPath: string, keyPath: string } | null} + */ +export function resolveTlsOptions( + env = process.env, + { readFileSync = fs.readFileSync, warn = (m) => console.warn(m) } = {} +) { + const certPath = typeof env?.OMNIROUTE_TLS_CERT === "string" ? env.OMNIROUTE_TLS_CERT.trim() : ""; + const keyPath = typeof env?.OMNIROUTE_TLS_KEY === "string" ? env.OMNIROUTE_TLS_KEY.trim() : ""; + + // Neither provided → plain HTTP, no warning (the common, default case). + if (!certPath && !keyPath) return null; + + // Only one of the pair → never half-enable TLS. Warn + fall back to HTTP. + if (!certPath || !keyPath) { + warn( + `[omniroute][tls] HTTPS not enabled: both OMNIROUTE_TLS_CERT and OMNIROUTE_TLS_KEY ` + + `are required (only ${certPath ? "cert" : "key"} provided). Serving HTTP.` + ); + return null; + } + + // Both provided → read them. A bad/unreadable path falls back to HTTP rather + // than crashing the server over a TLS misconfiguration. + try { + const cert = readFileSync(certPath); + const key = readFileSync(keyPath); + return { cert, key, certPath, keyPath }; + } catch (err) { + warn( + `[omniroute][tls] HTTPS not enabled: could not read TLS cert/key ` + + `(${err?.code || err?.message || String(err)}). Serving HTTP.` + ); + return null; + } +} + +/** + * Create either an `https.Server` (when `tlsOptions` is provided) or an + * `http.Server` (when it is `null`), forwarding the same request-listener / + * options arguments the caller would otherwise pass to `http.createServer`. + * + * When TLS is enabled, any leading options object is merged with `cert`/`key` + * and the trailing request listener is preserved, so existing wiring (WebSocket + * `upgrade` handling, request wrappers) keeps working over TLS unchanged. + * + * @param {any[]} args - the args originally passed to http.createServer (options?, listener?) + * @param {{ cert: Buffer|string, key: Buffer|string } | null} tlsOptions + * @param {{ createHttp?: Function, createHttps?: Function }} [deps] + * @returns {import("node:http").Server | import("node:https").Server} + */ +export function createServerListener( + args, + tlsOptions, + { createHttp = http.createServer.bind(http), createHttps = https.createServer.bind(https) } = {} +) { + const argList = Array.isArray(args) ? args : args === undefined ? [] : [args]; + + if (!tlsOptions) return createHttp(...argList); + + const { cert, key } = tlsOptions; + const lastFnIdx = argList.map((a) => typeof a === "function").lastIndexOf(true); + const listener = lastFnIdx >= 0 ? argList[lastFnIdx] : undefined; + const baseOpts = + argList[0] && typeof argList[0] === "object" && !Buffer.isBuffer(argList[0]) ? argList[0] : {}; + const merged = { ...baseOpts, cert, key }; + return listener ? createHttps(merged, listener) : createHttps(merged); +} diff --git a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx index b776f2455b..15224fb377 100644 --- a/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx +++ b/src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx @@ -22,6 +22,7 @@ import type { KeyStatus, KeyType } from "./apiManagerPageUtils"; import { readActiveOnlyPreference, writeActiveOnlyPreference } from "./apiManagerPageStorage"; import { buildApiKeyCreateScopes, mergeApiKeyPermissionScopes } from "./apiManagerScopes"; import { SELF_ACCOUNT_QUOTA_SCOPE, SELF_USAGE_SCOPE } from "@/shared/constants/selfServiceScopes"; +import { extractApiErrorMessage } from "@/shared/http/apiErrorMessage"; import { UsageLimitSettings } from "./components/UsageLimitSettings"; // Constants for validation @@ -550,7 +551,7 @@ export default function ApiManagerPageClient() { setNewKeyAllowUsageCommand(false); setShowAddModal(false); } else { - setCreateError(data.error || t("failedCreateKey")); + setCreateError(extractApiErrorMessage(data, t("failedCreateKey"))); } } catch (error) { console.error("Error creating key:", error); @@ -585,7 +586,7 @@ export default function ApiManagerPageClient() { setVisibleKeys((prev) => (prev.has(id) ? toggleKeyVisibility(prev, id) : prev)); } else { const data = await res.json(); - setPageError(data.error || t("failedDeleteKey")); + setPageError(extractApiErrorMessage(data, t("failedDeleteKey"))); } } catch (error) { console.error("Error deleting key:", error); @@ -609,7 +610,7 @@ export default function ApiManagerPageClient() { setCreatedKey(data.key); await fetchData(); } else { - setPageError(data.error || t("failedRegenerateKey")); + setPageError(extractApiErrorMessage(data, t("failedRegenerateKey"))); } } catch (error) { console.error("Error regenerating key:", error); @@ -785,7 +786,7 @@ export default function ApiManagerPageClient() { setEditingKey(null); } else { const data = await res.json(); - setPageError(data.error || t("failedUpdatePermissions")); + setPageError(extractApiErrorMessage(data, t("failedUpdatePermissions"))); } } catch (error) { console.error("Error updating permissions:", error); diff --git a/src/app/(dashboard)/dashboard/batch/page.tsx b/src/app/(dashboard)/dashboard/batch/page.tsx index b0d12bf5e1..0bf8b23a46 100644 --- a/src/app/(dashboard)/dashboard/batch/page.tsx +++ b/src/app/(dashboard)/dashboard/batch/page.tsx @@ -15,7 +15,7 @@ const BATCH_SUPPORTED = ["openai", "anthropic", "gemini"]; const MODEL_DEFAULTS: Record = { openai: ["gpt-4o-mini", "gpt-4o", "gpt-4-turbo"], anthropic: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"], - gemini: ["gemini-1.5-flash", "gemini-1.5-pro"], + gemini: ["gemini-2.5-flash", "gemini-2.5-pro"], }; const PROVIDER_NAMES: Record = { openai: "OpenAI", diff --git a/src/app/(dashboard)/dashboard/providers/[id]/__tests__/modelVisibilityToolbarActiveCount.test.tsx b/src/app/(dashboard)/dashboard/providers/[id]/__tests__/modelVisibilityToolbarActiveCount.test.tsx new file mode 100644 index 0000000000..6728d72911 --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/__tests__/modelVisibilityToolbarActiveCount.test.tsx @@ -0,0 +1,82 @@ +// @vitest-environment jsdom +// +// Regression test for issue #5264 — the "{active}/{total} active" model-count +// badge in the provider DETAIL page's "Available Models" toolbar. +// +// During the god-file decomposition in v3.8.13 (commit a25d5f1ef / PR #3327), +// ModelVisibilityToolbar kept receiving `activeCount`/`totalCount` props but the +// that rendered them was never carried over. The props were left +// orphaned (destructured as `_activeCount`/`_totalCount` to silence lint), so +// the count badge silently disappeared from the toolbar. +// +// This test renders the toolbar with activeCount={3} totalCount={5} and asserts +// the rendered output surfaces the `modelsActiveCount` interpolation ("3/5 +// active"). It fails against the pre-fix component (props unused → nothing +// rendered) and passes once the badge is restored. + +import React, { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { + ModelVisibilityToolbar, + type ModelVisibilityToolbarProps, +} from "../components/ModelRow"; + +// Minimal translator stub: no `has`, so providerText() falls back to +// interpolating the values into the fallback string ("{active}/{total} active"). +const t = ((key: string) => key) as ModelVisibilityToolbarProps["t"]; + +function buildProps( + overrides: Partial +): ModelVisibilityToolbarProps { + return { + t, + filterValue: "", + onFilterChange: vi.fn(), + activeCount: 0, + totalCount: 0, + onSelectAll: vi.fn(), + onDeselectAll: vi.fn(), + ...overrides, + }; +} + +const roots: Array<{ root: ReturnType; el: HTMLDivElement }> = []; + +function render(props: ModelVisibilityToolbarProps): HTMLDivElement { + const el = document.createElement("div"); + document.body.appendChild(el); + const root = createRoot(el); + act(() => { + root.render(); + }); + roots.push({ root, el }); + return el; +} + +beforeEach(() => { + ( + globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } + ).IS_REACT_ACT_ENVIRONMENT = true; +}); + +afterEach(() => { + for (const { root, el } of roots.splice(0)) { + act(() => root.unmount()); + el.remove(); + } + vi.clearAllMocks(); +}); + +describe("ModelVisibilityToolbar active/total count badge (#5264)", () => { + it("renders the {active}/{total} active count", () => { + const el = render(buildProps({ activeCount: 3, totalCount: 5 })); + expect(el.textContent).toContain("3/5 active"); + }); + + it("reflects updated counts", () => { + const el = render(buildProps({ activeCount: 7, totalCount: 12 })); + expect(el.textContent).toContain("7/12 active"); + }); +}); diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/ModelRow.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/ModelRow.tsx index a24b686c23..79e3d658e7 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/ModelRow.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/ModelRow.tsx @@ -103,8 +103,8 @@ export function ModelVisibilityToolbar({ t, filterValue, onFilterChange, - activeCount: _activeCount, - totalCount: _totalCount, + activeCount, + totalCount, onSelectAll, onDeselectAll, selectAllDisabled, @@ -245,6 +245,12 @@ export function ModelVisibilityToolbar({ visibility_off {providerText(t, "hideAllModels", "Hide all")} + + {providerText(t, "modelsActiveCount", "{active}/{total} active", { + active: activeCount, + total: totalCount, + })} + ); } diff --git a/src/app/(dashboard)/dashboard/providers/services/components/ServiceLifecycleButtons.tsx b/src/app/(dashboard)/dashboard/providers/services/components/ServiceLifecycleButtons.tsx index fde8a4b4d6..3cf9d09725 100644 --- a/src/app/(dashboard)/dashboard/providers/services/components/ServiceLifecycleButtons.tsx +++ b/src/app/(dashboard)/dashboard/providers/services/components/ServiceLifecycleButtons.tsx @@ -13,6 +13,7 @@ type Action = "start" | "stop" | "restart" | "update" | "install"; export function ServiceLifecycleButtons({ name }: ServiceLifecycleButtonsProps) { const { data, mutate } = useServiceStatus(name); const [pending, setPending] = useState(null); + const [error, setError] = useState(null); const running = data?.state === "running"; const starting = data?.state === "starting"; @@ -21,46 +22,69 @@ export function ServiceLifecycleButtons({ name }: ServiceLifecycleButtonsProps) async function action(verb: Action) { setPending(verb); + setError(null); try { - await fetch(`/api/services/${name}/${verb}`, { method: "POST" }); + const res = await fetch(`/api/services/${name}/${verb}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + }); + if (!res.ok) { + const payload = (await res.json().catch(() => null)) as { + error?: { message?: string }; + } | null; + throw new Error(payload?.error?.message || `HTTP ${res.status}`); + } mutate(); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); } finally { setPending(null); } } if (notInstalled) { - return ( - - ); + return ; } return ( -
- - - - +
+
+ + + + +
+ {error &&

{error}

}
); + + function LifecycleButtonGroup({ error }: { error: string | null }) { + return ( +
+ + {error &&

{error}

} +
+ ); + } } diff --git a/src/app/(dashboard)/dashboard/providers/services/components/ServiceLogsPanel.tsx b/src/app/(dashboard)/dashboard/providers/services/components/ServiceLogsPanel.tsx index 556ce6e699..f0f5825a6e 100644 --- a/src/app/(dashboard)/dashboard/providers/services/components/ServiceLogsPanel.tsx +++ b/src/app/(dashboard)/dashboard/providers/services/components/ServiceLogsPanel.tsx @@ -29,7 +29,9 @@ function LogLineRow({ line }: { line: LogLine }) { export function ServiceLogsPanel({ name }: ServiceLogsPanelProps) { const [filterInput, setFilterInput] = useState(""); - const { lines, isPaused, togglePause, clear, setFilter } = useServiceLogs(name, { tail: 200 }); + const { lines, isPaused, error, togglePause, clear, setFilter } = useServiceLogs(name, { + tail: 200, + }); const bottomRef = useRef(null); // Auto-scroll to bottom on new lines unless paused @@ -89,7 +91,9 @@ export function ServiceLogsPanel({ name }: ServiceLogsPanelProps) {
- {lines.length === 0 ? ( + {error ? ( +

{error}

+ ) : lines.length === 0 ? (

No log output yet.

) : ( lines.map((l, i) => ) diff --git a/src/app/(dashboard)/dashboard/providers/services/hooks/useServiceLogs.ts b/src/app/(dashboard)/dashboard/providers/services/hooks/useServiceLogs.ts index ff348a4bdf..9ad436c828 100644 --- a/src/app/(dashboard)/dashboard/providers/services/hooks/useServiceLogs.ts +++ b/src/app/(dashboard)/dashboard/providers/services/hooks/useServiceLogs.ts @@ -16,6 +16,7 @@ interface UseServiceLogsOptions { interface UseServiceLogsResult { lines: LogLine[]; isPaused: boolean; + error: string | null; togglePause: () => void; clear: () => void; setFilter: (filter: string) => void; @@ -29,6 +30,7 @@ export function useServiceLogs( ): UseServiceLogsResult { const [lines, setLines] = useState([]); const [isPaused, setIsPaused] = useState(false); + const [error, setError] = useState(null); const [filter, setFilter] = useState(options.filter ?? ""); const pauseRef = useRef(false); const esRef = useRef(null); @@ -51,10 +53,16 @@ export function useServiceLogs( const es = new EventSource(url); esRef.current = es; + // Clear any prior error once the stream actually (re)connects, rather than + // calling setError synchronously in the effect body (which triggers a + // cascading render and is flagged by react-hooks/set-state-in-effect). + es.addEventListener("open", () => setError(null)); + es.addEventListener("snapshot", (e) => { try { const snapshot = JSON.parse(e.data) as LogLine[]; setLines(snapshot.slice(-MAX_LINES)); + setError(null); } catch {} }); @@ -66,14 +74,20 @@ export function useServiceLogs( const next = [...prev, line]; return next.length > MAX_LINES ? next.slice(-MAX_LINES) : next; }); + setError(null); } catch {} }); + es.onerror = () => { + setError(`Unable to stream ${name} logs. Check local access and service status.`); + es.close(); + }; + return () => { es.close(); esRef.current = null; }; }, [name, filter, options.tail]); - return { lines, isPaused, togglePause, clear, setFilter }; + return { lines, isPaused, error, togglePause, clear, setFilter }; } diff --git a/src/app/api/oauth/[provider]/[action]/route.ts b/src/app/api/oauth/[provider]/[action]/route.ts index 5b1d5fb179..4bf2a1ba24 100755 --- a/src/app/api/oauth/[provider]/[action]/route.ts +++ b/src/app/api/oauth/[provider]/[action]/route.ts @@ -9,7 +9,10 @@ import { pollForToken, resolveBrowserOAuthRedirectUri, } from "@/lib/oauth/providers"; -import { persistOAuthConnection } from "@/lib/oauth/connectionPersistence"; +import { + persistOAuthConnection, + buildOAuthConnectionCreatePayload, +} from "@/lib/oauth/connectionPersistence"; import { createDeviceFlowTicket, getDeviceFlowTicketStatus } from "@/lib/oauth/deviceFlowTickets"; import { createProviderConnection, @@ -497,13 +500,9 @@ export async function POST( } } if (!connection) { - connection = await createProviderConnection({ - provider, - authType: "oauth", - ...tokenData, - expiresAt, - testStatus: "active", - }); + connection = await createProviderConnection( + buildOAuthConnectionCreatePayload(provider, tokenData, expiresAt) + ); } // Auto sync to Cloud if enabled @@ -589,13 +588,9 @@ export async function POST( } } if (!connection) { - connection = await createProviderConnection({ - provider, - authType: "oauth", - ...result.tokens, - expiresAt, - testStatus: "active", - }); + connection = await createProviderConnection( + buildOAuthConnectionCreatePayload(provider, result.tokens, expiresAt) + ); } // Auto sync to Cloud if enabled @@ -722,13 +717,9 @@ export async function POST( } } if (!connection) { - connection = await createProviderConnection({ - provider, - authType: "oauth", - ...tokenData, - expiresAt, - testStatus: "active", - }); + connection = await createProviderConnection( + buildOAuthConnectionCreatePayload(provider, tokenData, expiresAt) + ); } await syncToCloudIfEnabled(); @@ -796,13 +787,9 @@ export async function POST( } } if (!connection) { - connection = await createProviderConnection({ - provider, - authType: "oauth", - ...tokenData, - expiresAt, - testStatus: "active", - }); + connection = await createProviderConnection( + buildOAuthConnectionCreatePayload(provider, tokenData, expiresAt) + ); } await syncToCloudIfEnabled(); diff --git a/src/app/api/services/9router/install/route.ts b/src/app/api/services/9router/install/route.ts index 7708062153..a2df024eef 100644 --- a/src/app/api/services/9router/install/route.ts +++ b/src/app/api/services/9router/install/route.ts @@ -11,7 +11,7 @@ const BodySchema = z.object({ export async function POST(request: Request): Promise { let body: unknown; try { - body = await request.json(); + body = request.body === null ? {} : await request.json(); } catch { return createErrorResponse({ status: 400, message: "Invalid JSON body" }); } diff --git a/src/app/api/services/[name]/logs/route.ts b/src/app/api/services/[name]/logs/route.ts index fe2602b180..c3c3a93a9a 100644 --- a/src/app/api/services/[name]/logs/route.ts +++ b/src/app/api/services/[name]/logs/route.ts @@ -23,6 +23,23 @@ const MAX_FILTER_LEN = 200; const encoder = new TextEncoder(); +async function getOrInitNamedSupervisor(name: string) { + const existing = getSupervisor(name); + if (existing) return existing; + + if (name === "cliproxy") { + const { getOrInitSupervisor } = await import("../../cliproxy/_lib"); + return getOrInitSupervisor(); + } + + if (name === "9router") { + const { getOrInitSupervisor } = await import("../../9router/_lib"); + return getOrInitSupervisor(); + } + + return null; +} + function sseChunk(event: string, data: unknown): Uint8Array { return encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); } @@ -30,7 +47,7 @@ function sseChunk(event: string, data: unknown): Uint8Array { export async function GET(request: NextRequest, { params }: { params: Promise<{ name: string }> }) { const { name } = await params; - const supervisor = getSupervisor(name); + const supervisor = await getOrInitNamedSupervisor(name); if (!supervisor) { return createErrorResponse({ status: 404, message: `Service '${name}' not found` }); } diff --git a/src/app/api/services/cliproxy/install/route.ts b/src/app/api/services/cliproxy/install/route.ts index 765323d2de..591489cc24 100644 --- a/src/app/api/services/cliproxy/install/route.ts +++ b/src/app/api/services/cliproxy/install/route.ts @@ -11,7 +11,7 @@ const BodySchema = z.object({ export async function POST(request: Request): Promise { let body: unknown; try { - body = await request.json(); + body = request.body === null ? {} : await request.json(); } catch { return createErrorResponse({ status: 400, message: "Invalid JSON body" }); } diff --git a/src/app/api/v1/providers/[provider]/models/route.ts b/src/app/api/v1/providers/[provider]/models/route.ts index 6dd209c7f9..0accaf2ac4 100644 --- a/src/app/api/v1/providers/[provider]/models/route.ts +++ b/src/app/api/v1/providers/[provider]/models/route.ts @@ -1,4 +1,5 @@ import { getUnifiedModelsResponse } from "@/app/api/v1/models/catalog"; +import { getServiceModels } from "@/lib/db/serviceModels"; import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts"; /** @@ -19,6 +20,20 @@ export async function OPTIONS() { */ export async function GET(request: Request, { params }: { params: Promise<{ provider: string }> }) { const { provider: rawProvider } = await params; + if (rawProvider === "cliproxyapi" || rawProvider === "9router") { + const models = getServiceModels(rawProvider).filter((model) => model.available !== false); + return Response.json({ + object: "list", + data: models.map((model) => ({ + object: model.object || "model", + owned_by: rawProvider, + ...model, + id: model.id, + parent: null, + })), + }); + } + const providerEntry = getRegistryEntry(rawProvider); let providerId = rawProvider; let providerAlias = rawProvider; diff --git a/src/app/api/v1/relay/chat/completions/route.ts b/src/app/api/v1/relay/chat/completions/route.ts index 5d97fd2b2f..32bae3ef08 100644 --- a/src/app/api/v1/relay/chat/completions/route.ts +++ b/src/app/api/v1/relay/chat/completions/route.ts @@ -18,11 +18,165 @@ import { hashToken, sanitizeForensicHeader, } from "./relaySecurity"; +import { + getBifrostRoutingConfig, + resolveRelayRoutingBackend, + shouldTryBifrost, + type BifrostRoutingConfig, +} from "./routingBackend"; +import type { RelayToken } from "@/lib/db/relayProxies"; const JSON_CORS_HEADERS = { ...CORS_HEADERS, "Content-Type": "application/json" } as const; const injectionGuard = createInjectionGuard(); +type RelayUsageStatus = "success" | "error"; + +function recordUsage( + tokenId: string, + request: Request, + startTime: number, + clientIp: string, + userAgent: string | null, + status: RelayUsageStatus, + statusCode: number +) { + recordRelayUsage(tokenId, { + requestId: request.headers.get("x-request-id") || undefined, + status, + statusCode, + latencyMs: Date.now() - startTime, + clientIp, + userAgent, + }); +} + +function finalizeReadableStream( + body: ReadableStream, + onFinalize: (error?: unknown) => void +): ReadableStream { + const reader = body.getReader(); + let finalized = false; + + const finalizeOnce = (error?: unknown) => { + if (finalized) return; + finalized = true; + onFinalize(error); + }; + + return new ReadableStream({ + async pull(controller) { + try { + const { done, value } = await reader.read(); + if (done) { + finalizeOnce(); + controller.close(); + return; + } + controller.enqueue(value); + } catch (error) { + finalizeOnce(error); + controller.error(error); + } + }, + async cancel(reason) { + try { + await reader.cancel(reason); + } finally { + finalizeOnce(reason); + } + }, + }); +} + +async function forwardToBifrost( + request: Request, + body: unknown, + token: RelayToken, + config: BifrostRoutingConfig, + startTime: number, + clientIp: string, + userAgent: string | null +): Promise { + const wantsStream = + Boolean((body as { stream?: boolean } | null)?.stream) && config.streamingEnabled; + const upstreamHeaders: Record = { + "Content-Type": "application/json", + "x-relay-token-id": token.id, + "x-relay-client-ip": clientIp, + }; + if (config.apiKey) { + upstreamHeaders.Authorization = `Bearer ${config.apiKey}`; + } + + const ac = new AbortController(); + let timedOut = false; + const tid = setTimeout(() => { + timedOut = true; + ac.abort(); + }, config.timeoutMs); + + try { + const upstream = await fetch(`${config.baseUrl}/v1/chat/completions`, { + method: "POST", + headers: upstreamHeaders, + body: JSON.stringify(body), + signal: ac.signal, + }); + clearTimeout(tid); + + const headers = new Headers(upstream.headers); + headers.set("X-Routed-By", "bifrost"); + headers.set("X-Routing-Backend", "bifrost"); + headers.set("X-Relay-Token", token.tokenPrefix + "..."); + if (!wantsStream) { + headers.set("Content-Type", upstream.headers.get("Content-Type") ?? "application/json"); + } + + if (wantsStream && upstream.body) { + const stream = finalizeReadableStream(upstream.body, (error) => { + recordUsage( + token.id, + request, + startTime, + clientIp, + userAgent, + error || upstream.status >= 500 ? "error" : "success", + upstream.status + ); + }); + + return new Response(stream, { + status: upstream.status, + headers, + }); + } + + recordUsage( + token.id, + request, + startTime, + clientIp, + userAgent, + upstream.status < 500 ? "success" : "error", + upstream.status + ); + + return new Response(upstream.body, { + status: upstream.status, + headers, + }); + } catch (error) { + clearTimeout(tid); + const isAbort = error instanceof Error && error.name === "AbortError"; + throw new Error( + isAbort + ? `Bifrost sidecar timed out after ${config.timeoutMs}ms` + : `Bifrost sidecar unreachable: ${error instanceof Error ? error.message : String(error)}` + ); + } +} + export async function OPTIONS() { return handleCorsOptions(); } @@ -112,11 +266,13 @@ export async function POST(request: Request) { // 3. Clone request and forward to internal handler const cloned = request.clone(); + let parsedBody: unknown = null; + // Prompt injection guard (same as main endpoint) try { - const body = await cloned.json().catch(() => null); - if (body) { - const { blocked, result } = injectionGuard(body); + parsedBody = await cloned.json().catch(() => null); + if (parsedBody) { + const { blocked, result } = injectionGuard(parsedBody); if (blocked) { recordRelayUsage(token.id, { requestId: request.headers.get("x-request-id") || undefined, @@ -142,7 +298,7 @@ export async function POST(request: Request) { // Check allowed models const allowedModels: string[] = JSON.parse(token.allowedModels); if (allowedModels.length > 0 && !allowedModels.includes("*")) { - const model = (body as { model?: string }).model || ""; + const model = (parsedBody as { model?: string }).model || ""; const allowed = allowedModels.some( (p) => model === p || (p.endsWith("*") && model.startsWith(p.slice(0, -1))) ); @@ -162,6 +318,38 @@ export async function POST(request: Request) { // Continue even if guard fails } + const backend = resolveRelayRoutingBackend(); + const bifrostConfig = getBifrostRoutingConfig(); + if (shouldTryBifrost(backend, bifrostConfig)) { + try { + return await forwardToBifrost( + request, + parsedBody, + token, + bifrostConfig, + startTime, + clientIp, + userAgent + ); + } catch (error) { + if (backend === "bifrost") { + recordUsage(token.id, request, startTime, clientIp, userAgent, "error", 502); + return new Response( + JSON.stringify( + buildErrorBody(502, error instanceof Error ? error.message : String(error)) + ), + { + status: 502, + headers: { + ...JSON_CORS_HEADERS, + "X-Bifrost-Fallback": "/api/v1/relay/chat/completions", + }, + } + ); + } + } + } + // 4. Proxy to internal handler const originalRequest = new Request( request.url.replace("/relay/chat/completions", "/chat/completions"), @@ -183,6 +371,10 @@ export async function POST(request: Request) { // Add relay headers const newHeaders = new Headers(response.headers); newHeaders.set("X-Relay-Token", token.tokenPrefix + "..."); + newHeaders.set("X-Routing-Backend", "ts"); + if (backend === "auto" && bifrostConfig?.enabled) { + newHeaders.set("X-Routing-Fallback", "bifrost"); + } return new Response(response.body, { status: response.status, diff --git a/src/app/api/v1/relay/chat/completions/routingBackend.ts b/src/app/api/v1/relay/chat/completions/routingBackend.ts new file mode 100644 index 0000000000..36d4a6eae0 --- /dev/null +++ b/src/app/api/v1/relay/chat/completions/routingBackend.ts @@ -0,0 +1,41 @@ +export type RelayRoutingBackend = "ts" | "bifrost" | "auto"; + +const VALID_BACKENDS = new Set(["ts", "bifrost", "auto"]); + +export interface BifrostRoutingConfig { + baseUrl: string; + apiKey?: string; + timeoutMs: number; + streamingEnabled: boolean; + enabled: boolean; +} + +export function getBifrostRoutingConfig(env: NodeJS.ProcessEnv = process.env): BifrostRoutingConfig | null { + const baseUrl = env.BIFROST_BASE_URL?.replace(/\/$/, ""); + if (!baseUrl) return null; + const timeoutMs = Number.parseInt(env.BIFROST_TIMEOUT_MS || "", 10); + + return { + baseUrl, + apiKey: env.BIFROST_API_KEY || env.OMNIROUTE_BIFROST_KEY || undefined, + timeoutMs: Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 30000, + streamingEnabled: env.BIFROST_STREAMING_ENABLED !== "0", + enabled: env.BIFROST_ENABLED !== "0", + }; +} + +export function resolveRelayRoutingBackend(env: NodeJS.ProcessEnv = process.env): RelayRoutingBackend { + const configured = env.OMNIROUTE_RELAY_BACKEND || env.RELAY_ROUTING_BACKEND; + if (configured && VALID_BACKENDS.has(configured as RelayRoutingBackend)) { + return configured as RelayRoutingBackend; + } + + return getBifrostRoutingConfig(env)?.enabled ? "auto" : "ts"; +} + +export function shouldTryBifrost( + backend: RelayRoutingBackend, + config: BifrostRoutingConfig | null +): config is BifrostRoutingConfig { + return Boolean(config?.enabled && backend !== "ts"); +} diff --git a/src/lib/build-profile/featureDisabled.ts b/src/lib/build-profile/featureDisabled.ts index 1e2643c2cc..6f3788907b 100644 --- a/src/lib/build-profile/featureDisabled.ts +++ b/src/lib/build-profile/featureDisabled.ts @@ -21,6 +21,3 @@ export class FeatureDisabledError extends Error { export function featureDisabledError(featureName: string): FeatureDisabledError { return new FeatureDisabledError(featureName); } - -export const OMNIROUTE_BUILD_PROFILE: string = process.env.OMNIROUTE_BUILD_PROFILE || "full"; -export const IS_MINIMAL_BUILD: boolean = OMNIROUTE_BUILD_PROFILE === "minimal"; diff --git a/src/lib/combos/builderDraft.ts b/src/lib/combos/builderDraft.ts index 31ec199281..1638f1b35c 100644 --- a/src/lib/combos/builderDraft.ts +++ b/src/lib/combos/builderDraft.ts @@ -49,13 +49,6 @@ export function parseQualifiedModel( }; } -export function getComboDraftTarget(entry: unknown): string | null { - if (typeof entry === "string") return toTrimmedString(entry); - if (!isRecord(entry)) return null; - if (entry.kind === "combo-ref") return toTrimmedString(entry.comboName); - return toTrimmedString(entry.model); -} - export function buildPrecisionComboModelStep({ providerId, modelId, diff --git a/src/lib/combos/steps.ts b/src/lib/combos/steps.ts index 1487ddfa79..0b490e369a 100644 --- a/src/lib/combos/steps.ts +++ b/src/lib/combos/steps.ts @@ -142,14 +142,6 @@ function shouldTreatAsComboRef( return comboNames.has(target) || target === toTrimmedString(options.comboName); } -export function isComboRefStep(value: unknown): value is ComboRefStep { - return isRecord(value) && value.kind === "combo-ref" && !!toTrimmedString(value.comboName); -} - -export function isComboModelStep(value: unknown): value is ComboModelStep { - return isRecord(value) && value.kind === "model" && !!toTrimmedString(value.model); -} - export function getComboStepWeight(value: unknown): number { if (typeof value === "string") return 0; if (!isRecord(value)) return 0; diff --git a/src/lib/config/runtimeSettings.ts b/src/lib/config/runtimeSettings.ts index b589167505..406a4bdbc9 100644 --- a/src/lib/config/runtimeSettings.ts +++ b/src/lib/config/runtimeSettings.ts @@ -477,7 +477,9 @@ export async function applyRuntimeSettings( if (force || hasChanged(currentSnapshot.usageTokenBuffer, previousSnapshot.usageTokenBuffer)) { const newBuffer = - typeof currentSnapshot.usageTokenBuffer === "number" ? currentSnapshot.usageTokenBuffer : null; + typeof currentSnapshot.usageTokenBuffer === "number" + ? currentSnapshot.usageTokenBuffer + : null; await applyUsageTrackingSection(newBuffer); markChanged("usageTracking"); } @@ -531,7 +533,10 @@ export async function applyRuntimeSettings( markChanged("authzBypass"); } - if (force || hasChanged(currentSnapshot.customBannedSignals, previousSnapshot.customBannedSignals)) { + if ( + force || + hasChanged(currentSnapshot.customBannedSignals, previousSnapshot.customBannedSignals) + ) { setCustomBannedSignals(currentSnapshot.customBannedSignals); markChanged("bannedSignals"); } @@ -540,10 +545,6 @@ export async function applyRuntimeSettings( return changes; } -export function getLastAppliedRuntimeSettingsSnapshotForTests() { - return lastAppliedSnapshot; -} - export function resetRuntimeSettingsStateForTests() { lastAppliedSnapshot = null; currentAuthzBypass = DEFAULT_AUTHZ_BYPASS_SNAPSHOT; diff --git a/src/lib/db/commandCodeAuth.ts b/src/lib/db/commandCodeAuth.ts index 29cdbac313..0477eb7934 100644 --- a/src/lib/db/commandCodeAuth.ts +++ b/src/lib/db/commandCodeAuth.ts @@ -207,14 +207,3 @@ export function consumeCommandCodeAuthSecret( }; })() as ConsumedCommandCodeAuthSecret | null; } - -export function cleanupExpiredCommandCodeAuthSessions(now = nowIso()): number { - const result = db() - .prepare( - `UPDATE command_code_auth_sessions - SET status = 'expired', updated_at = ? - WHERE status IN ('pending', 'received') AND expires_at <= ?` - ) - .run(now, now); - return result.changes ?? 0; -} diff --git a/src/lib/db/encryption.ts b/src/lib/db/encryption.ts index 960ac32870..1559c14bb5 100644 --- a/src/lib/db/encryption.ts +++ b/src/lib/db/encryption.ts @@ -241,40 +241,6 @@ export function decryptConnectionFields }; } -/** - * Remove a specific listener. - */ -export function off( - event: E, - listener: DashboardEventListener -): void { - getBusState() - .listeners.get(event) - ?.delete(listener as Function); -} - -/** - * Remove all listeners for an event. - */ -export function removeAllListeners(event?: DashboardEventName): void { - const state = getBusState(); - if (event) { - state.listeners.delete(event); - } else { - state.listeners.clear(); - state.wildcardListeners.clear(); - } -} - -/** - * Get bus stats for monitoring. - */ -export function getBusStats(): { - totalListeners: number; - eventsWithListeners: number; - totalEmitted: number; - historySize: number; -} { - const state = getBusState(); - let totalListeners = state.wildcardListeners.size; - for (const set of state.listeners.values()) { - totalListeners += set.size; - } - return { - totalListeners, - eventsWithListeners: state.listeners.size, - totalEmitted: state.emitCount, - historySize: state.history.length, - }; -} - /** * Initialize event bus (idempotent). */ diff --git a/src/lib/memory/embedding/index.ts b/src/lib/memory/embedding/index.ts index 60c44b2061..782cf4fbda 100644 --- a/src/lib/memory/embedding/index.ts +++ b/src/lib/memory/embedding/index.ts @@ -15,16 +15,10 @@ import type { import { embedRemote } from "./remote"; import { embedStatic } from "./staticPotion"; import { embedTransformers } from "./transformersLocal"; -import { - buildCacheKey, - get as cacheGet, - set as cacheSet, - invalidate as cacheInvalidate, -} from "./cache"; +import { buildCacheKey, get as cacheGet, set as cacheSet } from "./cache"; const STATIC_MODEL = process.env.MEMORY_STATIC_MODEL || "minishlab/potion-base-8M"; -const TRANSFORMERS_MODEL = - process.env.MEMORY_TRANSFORMERS_MODEL || "Xenova/all-MiniLM-L6-v2"; +const TRANSFORMERS_MODEL = process.env.MEMORY_TRANSFORMERS_MODEL || "Xenova/all-MiniLM-L6-v2"; /** Build an EmbeddingResolution for "no source available" cases. */ function noSource(reason: string): EmbeddingResolution { @@ -184,12 +178,7 @@ export async function embed( }; } - const cacheKey = buildCacheKey( - resolution.source, - resolution.model, - resolution.dimensions, - text - ); + const cacheKey = buildCacheKey(resolution.source, resolution.model, resolution.dimensions, text); const cached = cacheGet(cacheKey); if (cached) { @@ -290,11 +279,3 @@ export async function listEmbeddingProviders(): Promise { const interval = settings.modelsDevSyncInterval as number | undefined; startPeriodicSync(interval); } - -/** - * Get context window limit for a specific model from synced capabilities. - * Returns null if not available. - */ -export function getModelContextLimit(provider: string, modelId: string): number | null { - const caps = getSyncedCapabilities(provider, modelId); - return caps[provider]?.[modelId]?.limit_context ?? null; -} diff --git a/src/lib/oauth/connectionPersistence.ts b/src/lib/oauth/connectionPersistence.ts index 4bbffec13f..5ecc4e6b90 100644 --- a/src/lib/oauth/connectionPersistence.ts +++ b/src/lib/oauth/connectionPersistence.ts @@ -27,6 +27,34 @@ function safeEqual(a: string | null | undefined, b: string | null | undefined): return timingSafeEqual(ba, bb); } +/** + * Build the create payload for a brand-new OAuth connection. + * + * #5326: mirror the freshly computed `expiresAt` into `tokenExpiresAt` at creation + * time. The dashboard token-health badge prefers `tokenExpiresAt` over `expiresAt` + * (ConnectionRow.tsx: `connection.tokenExpiresAt || connection.expiresAt`). If + * `tokenExpiresAt` stays null on a freshly created connection, the badge falls back + * to the original grant clock and can flash a false amber/"Token Expired" until the + * first background refresh writes both fields together. All refresh paths already + * persist `expiresAt` and `tokenExpiresAt` in lockstep + * (tokenHealthCheck onPersist, tokenRefresh.updateProviderCredentials); this makes + * creation consistent with them. + */ +export function buildOAuthConnectionCreatePayload( + provider: string, + tokenData: Record, + expiresAt: string | null +) { + return { + provider, + authType: "oauth" as const, + ...tokenData, + expiresAt, + tokenExpiresAt: expiresAt, + testStatus: "active" as const, + }; +} + async function syncToCloudIfEnabled(): Promise { try { const cloudEnabled = await isCloudEnabled(); @@ -76,13 +104,9 @@ export async function persistOAuthConnection( } } if (!connection) { - connection = await createProviderConnection({ - provider, - authType: "oauth", - ...tokenData, - expiresAt, - testStatus: "active", - }); + connection = await createProviderConnection( + buildOAuthConnectionCreatePayload(provider, tokenData, expiresAt) + ); } await syncToCloudIfEnabled(); diff --git a/src/lib/oauth/providers.ts b/src/lib/oauth/providers.ts index ab1ac80021..0a3ff407ec 100644 --- a/src/lib/oauth/providers.ts +++ b/src/lib/oauth/providers.ts @@ -99,13 +99,6 @@ export function getProvider(name) { return provider; } -/** - * Get all provider names - */ -export function getProviderNames() { - return Object.keys(PROVIDERS); -} - /** * Generate auth data for a provider. * diff --git a/src/lib/providers/catalog.ts b/src/lib/providers/catalog.ts index d7c35c17ca..7454acc564 100644 --- a/src/lib/providers/catalog.ts +++ b/src/lib/providers/catalog.ts @@ -173,16 +173,6 @@ export function getStaticProviderCatalogGroup( return STATIC_PROVIDER_CATALOG_GROUPS[category]; } -export function getStaticProviderCategories(providerId: string): StaticProviderCatalogCategory[] { - const categories: StaticProviderCatalogCategory[] = []; - for (const category of STATIC_PROVIDER_CATALOG_RESOLUTION_ORDER) { - if (STATIC_PROVIDER_CATALOG_GROUPS[category].providers[providerId]) { - categories.push(category); - } - } - return categories; -} - export function resolveStaticProviderCatalogEntry( providerId: string ): ResolvedStaticProviderCatalogEntry | null { diff --git a/src/lib/providers/validation/searchProviders.ts b/src/lib/providers/validation/searchProviders.ts index cc1aca1809..cc27aafd96 100644 --- a/src/lib/providers/validation/searchProviders.ts +++ b/src/lib/providers/validation/searchProviders.ts @@ -6,21 +6,6 @@ import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuard"; import { withCustomUserAgent } from "./headers"; import { toValidationErrorResult, validationWrite } from "./transport"; -export async function validateGenericProvider( - baseUrl: string, - apiKey: string, - providerSpecificData: any = {}, - provider: string, - isLocal: boolean = false -) { - const config = SEARCH_VALIDATOR_CONFIGS[provider]; - if (!config) { - return { valid: false, error: "Validator not found", unsupported: true }; - } - const { url, init } = config(apiKey, providerSpecificData); - return validateSearchProvider(url, init, providerSpecificData, isLocal); -} - export async function validateSearchProvider( url: string, init: RequestInit, diff --git a/src/lib/providers/validation/webProvidersA.ts b/src/lib/providers/validation/webProvidersA.ts index 982475eeb5..c75b8e15d6 100644 --- a/src/lib/providers/validation/webProvidersA.ts +++ b/src/lib/providers/validation/webProvidersA.ts @@ -182,6 +182,17 @@ export function isGrokAntiBotBlock(body: string | null | undefined): boolean { return false; } +// Shared IP-reputation / anti-bot guidance (#3474, #5350). The request was rejected +// before (or independently of) auth — the cookie itself is likely fine. cf_clearance +// is pinned to the IP + TLS fingerprint + User-Agent that earned it and cannot be +// replayed from a different machine/IP, so an auth-shaped rejection after a +// cf_clearance was supplied is almost always this block, not a bad cookie. +const GROK_IP_REPUTATION_GUIDANCE = + "Your sso cookie is likely fine — this is an IP-reputation block on the request, not an " + + "auth failure. cf_clearance is pinned to the IP + TLS fingerprint + User-Agent that earned " + + "it and cannot be replayed from a different machine/IP. Retry from a residential IP or " + + "configure a proxy for grok-web."; + export async function validateGrokWebProvider({ apiKey, providerSpecificData = {} }: any) { try { const token = extractCookieValue(apiKey, "sso"); @@ -291,7 +302,17 @@ export async function validateGrokWebProvider({ apiKey, providerSpecificData = { return { valid: true, error: null }; } + // Did the user actually supply a cf_clearance cookie? Detect it from the raw + // input blob via a real cookie-pair match — NOT extractCookieValue, which + // returns the whole bare value for any name when the input has no ";" (#5350). + const suppliedCfClearance = /(?:^|;\s*)cf_clearance=[^;\s]+/.test(String(apiKey || "")); + if (response.status === 401) { + // With a cf_clearance supplied, a 401 is almost always an IP-reputation block + // (the clearance can't be replayed from a different machine), not a bad cookie. + if (suppliedCfClearance) { + return { valid: false, error: `Grok returned 401. ${GROK_IP_REPUTATION_GUIDANCE}` }; + } return { valid: false, error: "Invalid SSO cookie — re-paste from grok.com DevTools → Cookies → sso", @@ -304,8 +325,14 @@ export async function validateGrokWebProvider({ apiKey, providerSpecificData = { // messaging — a misleading "invalid cookie" verdict on an IP-reputation // block (issue #3474) sends users chasing a cookie that is actually fine. // - // 1. Auth-shaped → the cookie/session is the problem; re-paste it. + // 1. Auth-shaped → the cookie/session is the problem; re-paste it. But when a + // cf_clearance was supplied, this is almost always an IP-reputation block the + // edge surfaced as an auth failure — the clearance can't be replayed from a + // different machine, so re-pasting the cookie will not help (#5350). if (/invalid-credentials|unauthenticated|unauthorized/i.test(errorDetail)) { + if (suppliedCfClearance) { + return { valid: false, error: `Grok returned 403. ${GROK_IP_REPUTATION_GUIDANCE}` }; + } return { valid: false, error: "Invalid SSO cookie — re-paste from grok.com DevTools → Cookies → sso", @@ -319,10 +346,7 @@ export async function validateGrokWebProvider({ apiKey, providerSpecificData = { if (isCloudflareChallenge(errorDetail) || isGrokAntiBotBlock(errorDetail)) { return { valid: false, - error: - "Grok returned 403 (anti-bot/Cloudflare block). Your sso cookie is likely fine — " + - "this is an IP-reputation block on the request, not an auth failure. Retry from a " + - "residential IP or configure a proxy for grok-web.", + error: `Grok returned 403 (anti-bot/Cloudflare block). ${GROK_IP_REPUTATION_GUIDANCE}`, }; } // 3. Structured upstream error (e.g. probe model renamed) → surface the body diff --git a/src/lib/providers/webCookieAuth.ts b/src/lib/providers/webCookieAuth.ts index a7e204efdf..5236bc0ab2 100644 --- a/src/lib/providers/webCookieAuth.ts +++ b/src/lib/providers/webCookieAuth.ts @@ -49,6 +49,12 @@ export function extractCookieValue(rawValue: string, cookieName: string): string * only appended when it appears as a real cookie pair in the input, so a bare * `sso` value (no `;`/`=`) is never mistaken for an `sso-rw` value. * + * The Cloudflare cookies `cf_clearance` and `__cf_bm` are forwarded the same + * way when present (#5350) — Cloudflare on grok.com expects the same clearance + * the browser earned, and AIClient2API forwards them too. Like `sso-rw`, each is + * appended only when it appears as a real cookie pair, so a bare `sso` blob + * still produces exactly `sso=` (no phantom cf keys). + * * Returns "" when no `sso` value can be extracted. */ export function buildGrokCookieHeader(rawValue: string): string { @@ -56,9 +62,11 @@ export function buildGrokCookieHeader(rawValue: string): string { if (!sso) return ""; const parts = [`sso=${sso}`]; - if (/(?:^|;\s*)sso-rw=/.test(rawValue)) { - const ssoRw = extractCookieValue(rawValue, "sso-rw"); - if (ssoRw) parts.push(`sso-rw=${ssoRw}`); + for (const name of ["sso-rw", "cf_clearance", "__cf_bm"]) { + if (new RegExp("(?:^|;\\s*)" + name + "=").test(rawValue)) { + const value = extractCookieValue(rawValue, name); + if (value) parts.push(`${name}=${value}`); + } } return parts.join("; "); } diff --git a/src/lib/proxyLogger.ts b/src/lib/proxyLogger.ts index 19cd565f14..3027419dd2 100644 --- a/src/lib/proxyLogger.ts +++ b/src/lib/proxyLogger.ts @@ -238,14 +238,3 @@ export function clearProxyLogs() { } } } - -// ──────────────── Stats ──────────────── - -export function getProxyLogStats() { - const total = proxyLogs.length; - const success = proxyLogs.filter((l) => l.status === "success").length; - const error = proxyLogs.filter((l) => l.status === "error").length; - const timeout = proxyLogs.filter((l) => l.status === "timeout").length; - const direct = proxyLogs.filter((l) => l.level === "direct").length; - return { total, success, error, timeout, direct }; -} diff --git a/src/lib/quota/sqliteQuotaStore.ts b/src/lib/quota/sqliteQuotaStore.ts index 3849f0112d..dab40da91f 100644 --- a/src/lib/quota/sqliteQuotaStore.ts +++ b/src/lib/quota/sqliteQuotaStore.ts @@ -15,13 +15,7 @@ * Part of: Group B — Quota Sharing Engine (plan 22, frente F6). */ -import { - getPool, - getBucket, - incrementBucket, - getPair, - sumPoolDimension, -} from "@/lib/localDb"; +import { getPool, getBucket, incrementBucket, getPair, sumPoolDimension } from "@/lib/localDb"; import { WINDOW_MS, dimensionKeyToString } from "./dimensions"; import type { DimensionKey } from "./dimensions"; import type { QuotaStore, PoolUsageSnapshot } from "./types"; @@ -286,7 +280,3 @@ export function getSqliteQuotaStore(): SqliteQuotaStore { } return _instance; } - -export function resetSqliteQuotaStore(): void { - _instance = null; -} diff --git a/src/lib/semanticCache.ts b/src/lib/semanticCache.ts index 0410844678..b185b19963 100644 --- a/src/lib/semanticCache.ts +++ b/src/lib/semanticCache.ts @@ -116,7 +116,13 @@ function getMemoryCache() { * @param {string} [apiKeyId] - API key ID for per-key isolation (prevents cross-user cache hits) * @returns {string} hex signature */ -export function generateSignature(model, conversation, temperature = 0, topP = 1, apiKeyId?: string) { +export function generateSignature( + model, + conversation, + temperature = 0, + topP = 1, + apiKeyId?: string +) { const payload = JSON.stringify({ model, messages: normalizeConversation(conversation), @@ -247,24 +253,6 @@ export function setCachedResponse(signature, model, response, tokensSaved = 0, t } } -// ─── Maintenance ───────────────── - -/** - * Remove expired entries from SQLite. - * @returns {number} Number of entries removed - */ -export function cleanExpiredEntries() { - try { - const db = getDbInstance(); - const result = db - .prepare("DELETE FROM semantic_cache WHERE expires_at <= datetime('now')") - .run(); - return result.changes; - } catch { - return 0; - } -} - /** * Invalidate cache entries by model name. * Useful when a model is updated/changed and cached responses are stale. @@ -315,48 +303,6 @@ export function invalidateStale(maxAgeMs: number): number { } } -// ── Auto-cleanup timer ── - -let _cleanupTimer: ReturnType | null = null; - -/** - * Start periodic auto-cleanup of expired entries. - * @param {number} intervalMs - Cleanup interval (default: 5 minutes) - */ -export function startAutoCleanup(intervalMs = 300_000): void { - stopAutoCleanup(); - _cleanupTimer = setInterval(() => { - const removed = cleanExpiredEntries(); - if (removed > 0) { - console.log(`[SemanticCache] Auto-cleaned ${removed} expired entries`); - } - }, intervalMs); - if (_cleanupTimer && typeof _cleanupTimer === "object" && "unref" in _cleanupTimer) { - (_cleanupTimer as { unref?: () => void }).unref?.(); - } -} - -/** - * Stop periodic auto-cleanup. - */ -export function stopAutoCleanup(): void { - if (_cleanupTimer) { - clearInterval(_cleanupTimer); - _cleanupTimer = null; - } -} - -export function cleanOldMetrics(retentionDays = 90): number { - try { - const db = getDbInstance(); - const cutoff = new Date(Date.now() - retentionDays * 86400000).toISOString(); - const result = db.prepare("DELETE FROM semantic_cache WHERE created_at < ?").run(cutoff); - return result.changes || 0; - } catch { - return 0; - } -} - /** * Clear all cache entries. */ diff --git a/src/lib/services/registry.ts b/src/lib/services/registry.ts index 556309b60b..0f1cd9664a 100644 --- a/src/lib/services/registry.ts +++ b/src/lib/services/registry.ts @@ -12,10 +12,6 @@ export function getSupervisor(tool: string): ServiceSupervisor | null { return supervisors.get(tool) ?? null; } -export function listSupervisors(): ServiceSupervisor[] { - return Array.from(supervisors.values()); -} - /** Remove a supervisor by tool name. Intended for use in tests. */ export function unregisterSupervisor(tool: string): void { supervisors.delete(tool); diff --git a/src/lib/tokenHealthCheck.ts b/src/lib/tokenHealthCheck.ts index 80316fb682..5760e41a75 100644 --- a/src/lib/tokenHealthCheck.ts +++ b/src/lib/tokenHealthCheck.ts @@ -341,7 +341,36 @@ export async function checkConnection(conn) { const intervalMin = conn.healthCheckInterval ?? DEFAULT_HEALTH_CHECK_INTERVAL_MIN; if (intervalMin <= 0) return; if (!conn.isActive) return; - if (!conn.refreshToken || typeof conn.refreshToken !== "string") return; + if (!conn.refreshToken || typeof conn.refreshToken !== "string") { + // #5326: a refresh-CAPABLE provider (e.g. antigravity/gemini) with no usable + // refresh token can never self-heal via the sweep — it genuinely needs re-auth. + // Silently skipping here left the row at testStatus="active" while the dashboard + // badge (which derives expiry from tokenExpiresAt||expiresAt) showed a confusing + // cosmetic "Token Expired". Surface reality as a terminal "expired" status instead. + // Guard tightly so we do NOT clobber: + // - providers that simply don't use refresh tokens (supportsTokenRefresh=false) + // - connections already in a terminal/specific state (expired/banned/credits_exhausted) + // - transient cooldown state (unavailable) owned by the request path + const refreshCapableNeedsReauth = + supportsTokenRefresh(conn.provider) && + (!conn.testStatus || conn.testStatus === "active"); + if (refreshCapableNeedsReauth) { + const now = new Date().toISOString(); + await updateProviderConnection(conn.id, { + testStatus: "expired", + lastHealthCheckAt: now, + lastError: "No refresh token available — re-authenticate this account.", + lastErrorAt: now, + lastErrorType: "no_refresh_token", + lastErrorSource: "oauth", + errorCode: "no_refresh_token", + }); + log( + `${LOG_PREFIX} ${conn.provider}/${getConnectionLogLabel(conn)} has no refresh token; marking expired (needs re-auth)` + ); + } + return; + } // Retry expired connections with exponential backoff up to EXPIRED_RETRY_MAX times. if (conn.testStatus === "expired") { diff --git a/src/lib/usage/callLogs.ts b/src/lib/usage/callLogs.ts index ada9efee37..048f75addd 100644 --- a/src/lib/usage/callLogs.ts +++ b/src/lib/usage/callLogs.ts @@ -425,28 +425,40 @@ function listReferencedArtifacts() { ); } +// #5217: SQLite caps a statement at SQLITE_MAX_VARIABLE_NUMBER bound params +// (~999 on many builds). Callers like trimCallLogsToMaxRows() passed up to 5000 +// ids in one `IN (...)` → "too many SQL variables" aborted trimming. Chunk well +// under the limit so each DELETE/SELECT stays valid. +const DELETE_ID_CHUNK_SIZE = 500; + function deleteCallLogRowsByIds(ids: string[]): DeleteResult { if (ids.length === 0) { return { deletedRows: 0, deletedArtifacts: 0 }; } const db = getDbInstance(); - const placeholders = ids.map(() => "?").join(", "); - const rows = db - .prepare(`SELECT artifact_relpath FROM call_logs WHERE id IN (${placeholders})`) - .all(...ids) as Array<{ artifact_relpath: string | null }>; - - const result = db.prepare(`DELETE FROM call_logs WHERE id IN (${placeholders})`).run(...ids); + let deletedRows = 0; let deletedArtifacts = 0; - for (const row of rows) { - if (deleteCallArtifact(row.artifact_relpath)) { - deletedArtifacts++; + + for (let i = 0; i < ids.length; i += DELETE_ID_CHUNK_SIZE) { + const chunk = ids.slice(i, i + DELETE_ID_CHUNK_SIZE); + const placeholders = chunk.map(() => "?").join(", "); + const rows = db + .prepare(`SELECT artifact_relpath FROM call_logs WHERE id IN (${placeholders})`) + .all(...chunk) as Array<{ artifact_relpath: string | null }>; + + const result = db.prepare(`DELETE FROM call_logs WHERE id IN (${placeholders})`).run(...chunk); + deletedRows += result.changes; + for (const row of rows) { + if (deleteCallArtifact(row.artifact_relpath)) { + deletedArtifacts++; + } } } cleanupEmptyCallLogDirs(); return { - deletedRows: result.changes, + deletedRows, deletedArtifacts, }; } diff --git a/src/lib/webhooks/eventDescriptions.ts b/src/lib/webhooks/eventDescriptions.ts index bac4e382c6..00ffae98ef 100644 --- a/src/lib/webhooks/eventDescriptions.ts +++ b/src/lib/webhooks/eventDescriptions.ts @@ -74,12 +74,3 @@ export const EVENT_DESCRIPTIONS: Record = { exampleData: { message: "Test ping from OmniRoute", webhookId: "preview" }, }, }; - -export function buildExamplePayload(event: WebhookEvent): Record { - return { - event, - webhook_id: "preview", - timestamp: new Date().toISOString(), - data: EVENT_DESCRIPTIONS[event].exampleData, - }; -} diff --git a/src/server-init.ts b/src/server-init.ts index b182d7b783..3df0aafa9f 100644 --- a/src/server-init.ts +++ b/src/server-init.ts @@ -10,6 +10,7 @@ import { startCleanupScheduler } from "./lib/db/cleanup"; import { getSettings } from "./lib/db/settings"; import { applyRuntimeSettings } from "./lib/config/runtimeSettings"; import { setSystemPromptConfig } from "@omniroute/open-sse/services/systemPrompt.ts"; +import { hydrateThinkingBudgetConfig } from "@omniroute/open-sse/services/thinkingBudget.ts"; import { startRuntimeConfigHotReload } from "./lib/config/hotReload"; import { startSpendBatchWriter } from "./lib/spend/batchWriter"; import { registerDefaultGuardrails } from "./lib/guardrails"; @@ -86,6 +87,14 @@ async function startServer() { startupLog.info("Global System Prompt restored from settings"); } + // Restore the proxy-level Thinking-Budget config (#5312). It lives in + // `settings.thinkingBudget` and is NOT covered by applyRuntimeSettings, so + // without this the dashboard mode (auto/custom/adaptive) silently reverts to + // the passthrough default on every restart. + if (hydrateThinkingBudgetConfig(settings)) { + startupLog.info("Thinking-Budget config restored from settings"); + } + // Initialize cloud sync startSpendBatchWriter(); registerDefaultGuardrails(); diff --git a/src/server/authz/classify.ts b/src/server/authz/classify.ts index d0721dc5ae..f8fc361565 100644 --- a/src/server/authz/classify.ts +++ b/src/server/authz/classify.ts @@ -3,7 +3,7 @@ import { PUBLIC_READONLY_API_ROUTE_PREFIXES, PUBLIC_READONLY_METHODS, } from "../../shared/constants/publicApiRoutes"; -import type { ClassificationReason, RouteClass, RouteClassification } from "./types"; +import type { ClassificationReason, RouteClassification } from "./types"; const CLIENT_API_ALIAS_PREFIXES: ReadonlyArray<{ alias: string; canonical: string }> = [ { alias: "/chat/completions", canonical: "/api/v1/chat/completions" }, @@ -139,15 +139,3 @@ function isClassifiedAsPublic(path: string, method: string): boolean { } return matchesReadonlyPublic(path, method); } - -export function isClientApi(routeClass: RouteClass): boolean { - return routeClass === "CLIENT_API"; -} - -export function isManagement(routeClass: RouteClass): boolean { - return routeClass === "MANAGEMENT"; -} - -export function isPublic(routeClass: RouteClass): boolean { - return routeClass === "PUBLIC"; -} diff --git a/src/server/authz/pipeline.ts b/src/server/authz/pipeline.ts index 413c4c4f9d..72257947ec 100644 --- a/src/server/authz/pipeline.ts +++ b/src/server/authz/pipeline.ts @@ -174,7 +174,9 @@ function invalidOriginResponse(requestId: string): NextResponse { { error: { code: "INVALID_ORIGIN", - message: "Invalid request origin", + message: + "Invalid request origin. If you reach the dashboard over a custom host or an HTTPS proxy, " + + "set OMNIROUTE_PUBLIC_BASE_URL to that exact URL and restart. Direct loopback/LAN-IP access works without it.", correlation_id: requestId, }, }, @@ -228,10 +230,24 @@ export async function runAuthzPipeline( const guardedPathname = classification.normalizedPath; const managementDashboardRoute = isManagementDashboardRoute(classification, pathname); + // Relax the CORS origin fallback ONLY for the token-authenticated API + // surface (CLIENT_API: /v1/*, /v1beta/*, codex/responses aliases) and + // read-only PUBLIC endpoints. These authenticate via Authorization / + // x-api-key headers that browsers never auto-attach, so echoing the caller's + // Origin (or `*`) there carries no credentialed-session / CSRF risk — it just + // lets browser/Electron clients (issue #5242) read responses they are already + // entitled to. MANAGEMENT (cookie-authed dashboard) and non-read-only PUBLIC + // routes (e.g. /api/cloud/, which sets Allow-Credentials in its own handler) + // stay exactly fail-closed. + const corsRelaxOrigin = + classification.routeClass === "CLIENT_API" || + (classification.routeClass === "PUBLIC" && + classification.reason === "public_readonly_prefix"); + if (guardedPathname.startsWith("/api/") && isDraining()) { const response = drainingResponse(requestId); stampRouteResponse(response, requestId, classification.routeClass); - applyCorsHeaders(response, request); + applyCorsHeaders(response, request, corsRelaxOrigin); return response; } @@ -243,7 +259,7 @@ export async function runAuthzPipeline( ); if (bodySizeRejection) { stampRouteResponse(bodySizeRejection, requestId, classification.routeClass); - applyCorsHeaders(bodySizeRejection, request); + applyCorsHeaders(bodySizeRejection, request, corsRelaxOrigin); return bodySizeRejection; } } @@ -282,7 +298,7 @@ export async function runAuthzPipeline( const preflight = new NextResponse(null, { status: 204 }); preflight.headers.set(AUTHZ_HEADER_REQUEST_ID, requestId); preflight.headers.set(AUTHZ_HEADER_ROUTE_CLASS, classification.routeClass); - applyCorsHeaders(preflight, request); + applyCorsHeaders(preflight, request, corsRelaxOrigin); return preflight; } @@ -290,7 +306,7 @@ export async function runAuthzPipeline( const response = NextResponse.next({ request: { headers: requestHeaders } }); response.headers.set(AUTHZ_HEADER_REQUEST_ID, requestId); response.headers.set(AUTHZ_HEADER_ROUTE_CLASS, classification.routeClass); - applyCorsHeaders(response, request); + applyCorsHeaders(response, request, corsRelaxOrigin); return response; } @@ -303,7 +319,7 @@ export async function runAuthzPipeline( } const rejection = rejectionResponse(outcome, classification, requestId); - applyCorsHeaders(rejection, request); + applyCorsHeaders(rejection, request, corsRelaxOrigin); return rejection; } @@ -326,7 +342,7 @@ export async function runAuthzPipeline( const response = NextResponse.next({ request: { headers: requestHeaders } }); response.headers.set(AUTHZ_HEADER_REQUEST_ID, requestId); response.headers.set(AUTHZ_HEADER_ROUTE_CLASS, classification.routeClass); - applyCorsHeaders(response, request); + applyCorsHeaders(response, request, corsRelaxOrigin); if (managementDashboardRoute) { await refreshDashboardSessionIfNeeded(response, request); } diff --git a/src/server/authz/routeGuard.ts b/src/server/authz/routeGuard.ts index e04daccf1f..10dc695e8d 100644 --- a/src/server/authz/routeGuard.ts +++ b/src/server/authz/routeGuard.ts @@ -139,6 +139,7 @@ export function classifyHostLocality(ip: string | null): "loopback" | "lan" | "r */ const PRIVATE_LAN_PATTERNS: ReadonlyArray = [ /^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, + /^100\.(6[4-9]|[78]\d|9\d|1[01]\d|12[0-7])\.\d{1,3}\.\d{1,3}$/, /^192\.168\.\d{1,3}\.\d{1,3}$/, /^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/, /^f[cd][0-9a-f]{2}:/i, // IPv6 ULA fc00::/7 diff --git a/src/server/cors/origins.ts b/src/server/cors/origins.ts index 2c1759f216..139966f85e 100644 --- a/src/server/cors/origins.ts +++ b/src/server/cors/origins.ts @@ -101,10 +101,32 @@ export function resolveAllowedOrigin(requestOrigin: string | null | undefined): * (rejections, preflight, normal `next()` continuations). When the origin * is not allowed, no `Access-Control-Allow-Origin` is added — browsers * will block the response, which is the desired fail-closed behavior. + * + * `relaxForTokenAuth` opts the response into a permissive origin fallback for + * the token-authenticated API surface (OpenAI/Anthropic-compatible `/v1/*` and + * `/v1beta/*`, plus read-only public endpoints). Those routes authenticate via + * `Authorization` / `x-api-key` headers that browsers NEVER auto-attach, so a + * permissive `Access-Control-Allow-Origin` there carries none of the + * credentialed-session / CSRF risk that the fail-closed default protects (that + * risk lives in cookie-authenticated MANAGEMENT/dashboard routes, which must + * pass `relaxForTokenAuth = false` and stay exactly fail-closed). When the + * explicit allowlist (`CORS_ALLOW_ALL` / `CORS_ALLOWED_ORIGINS` / runtime + * settings) does not match, the caller's `Origin` is echoed back (with + * `Vary: Origin`) so browser/Electron renderers can read the response, or `*` + * is returned when there is no `Origin` header. This is NEVER paired with + * `Access-Control-Allow-Credentials` (these routes are not cookie-authed), so + * the echo/wildcard stays safe. */ -export function applyCorsHeaders(response: Response, request: Request): void { +export function applyCorsHeaders( + response: Response, + request: Request, + relaxForTokenAuth: boolean = false +): void { const requestOrigin = request.headers.get("origin"); - const allowed = resolveAllowedOrigin(requestOrigin); + let allowed = resolveAllowedOrigin(requestOrigin); + if (allowed === null && relaxForTokenAuth) { + allowed = requestOrigin && requestOrigin.length > 0 ? requestOrigin : "*"; + } if (allowed !== null) { response.headers.set("Access-Control-Allow-Origin", allowed); response.headers.append("Vary", "Origin"); diff --git a/src/server/origin/publicOrigin.ts b/src/server/origin/publicOrigin.ts index 87a9431325..c925b8e166 100644 --- a/src/server/origin/publicOrigin.ts +++ b/src/server/origin/publicOrigin.ts @@ -2,7 +2,11 @@ import { classifyHostLocality } from "@/server/authz/routeGuard"; import { PEER_IP_HEADER } from "@/server/authz/headers"; import { resolveStampedPeer } from "@/server/authz/peerStamp"; -export type PublicOriginSource = "configured" | "trusted-forwarded" | "request-url"; +export type PublicOriginSource = + | "configured" + | "trusted-forwarded" + | "request-url" + | "direct-local-host"; export interface PublicOriginCandidate { origin: string; @@ -162,6 +166,56 @@ function requestUrlOrigin(request: Request): string | null { } } +function requestUrlProtocol(request: Request): "http" | "https" { + try { + return new URL(request.url).protocol === "https:" ? "https" : "http"; + } catch { + return "http"; + } +} + +/** + * Direct (no-proxy) LAN/loopback access (#5340). When the dashboard is reached + * over a private-network host — e.g. `http://192.168.0.15:20128` instead of the + * configured `localhost` base URL — Next.js standalone reports the internal bind + * host in `request.url`, so the browser's same-origin `Origin` matches no + * candidate and every mutation is rejected with INVALID_ORIGIN. + * + * This accepts the request `Host` (or a trusted `X-Forwarded-Host`) as a valid + * mutation origin, gated by TWO independent checks so it cannot be abused: + * 1. The token-stamped socket peer must be loopback or private-LAN (a public + * client never matches — it presents a public source IP). + * 2. The Host itself must be a loopback/private-LAN IP literal. A DNS-rebinding + * attacker carries a *domain* in the Host (classifies as "remote"), so a + * rebind to a loopback/LAN socket cannot turn an attacker domain into a + * trusted origin. Direct IP access over the LAN/loopback still works. + * The protocol is pinned to the actual connection (or a trusted forwarded proto), + * never derived from the attacker-controllable Origin. + */ +function directLocalHostOrigin(request: Request): string | null { + const peer = resolveStampedPeer( + request.headers.get(PEER_IP_HEADER), + process.env.OMNIROUTE_PEER_STAMP_TOKEN + ); + if (classifyHostLocality(peer) === "remote") return null; + + const rawHost = trustsForwardedHeaders(request) + ? firstHeaderValue(request.headers.get("x-forwarded-host")) ?? request.headers.get("host") + : request.headers.get("host"); + const host = sanitizeForwardedHost(rawHost); + if (!host) return null; + if (classifyHostLocality(host) === "remote") return null; + + const proto = + sanitizeForwardedProto(firstHeaderValue(request.headers.get("x-forwarded-proto"))) ?? + requestUrlProtocol(request); + try { + return normalizeOrigin(`${proto}://${host}`); + } catch { + return null; + } +} + export function getPublicOriginCandidates(request: Request): PublicOriginCandidate[] { const candidates: PublicOriginCandidate[] = []; @@ -176,6 +230,9 @@ export function getPublicOriginCandidates(request: Request): PublicOriginCandida if (forwarded) candidates.push({ origin: forwarded, source: "trusted-forwarded" }); } + const directLocal = directLocalHostOrigin(request); + if (directLocal) candidates.push({ origin: directLocal, source: "direct-local-host" }); + return uniqueCandidates(candidates); } diff --git a/src/shared/components/NoAuthAccountCard.tsx b/src/shared/components/NoAuthAccountCard.tsx index c02b5fa22b..d4e182c5c4 100644 --- a/src/shared/components/NoAuthAccountCard.tsx +++ b/src/shared/components/NoAuthAccountCard.tsx @@ -26,9 +26,30 @@ interface Connection { isActive?: boolean; } +interface InlineProxy { + type: string; + host: string; + port: number; + username?: string; + password?: string; +} + +// #5217 (Gap 1): an account proxy is now stored as EITHER a Proxy Pool reference +// (`proxyId`, resolved server-side so a pool edit propagates to every account) OR +// a one-off inline `proxy` (the "custom" escape hatch / legacy entries). interface AccountProxyConfig { fingerprint: string; - proxy: { type: string; host: string; port: number; username?: string; password?: string } | null; + proxy?: InlineProxy | null; + proxyId?: string | null; +} + +interface SavedProxy { + id: string; + name?: string; + type?: string; + host?: string; + port?: number | string; + status?: string; } const PROXY_TYPES = [ @@ -41,8 +62,26 @@ function getAccountProxies(conn: Connection | undefined): AccountProxyConfig[] { return (conn?.providerSpecificData?.accountProxies as AccountProxyConfig[]) || []; } -function getProxyForFingerprint(proxies: AccountProxyConfig[], fp: string) { - return proxies.find((p) => p.fingerprint === fp)?.proxy ?? null; +function getEntryForFingerprint(proxies: AccountProxyConfig[], fp: string) { + return proxies.find((p) => p.fingerprint === fp) ?? null; +} + +/** + * Resolve the proxy to DISPLAY for an account: a by-id reference is looked up in + * the Proxy Pool list, an inline proxy is shown directly. Returns null (direct) + * when there is no entry or the referenced pool proxy no longer exists. + */ +function getDisplayProxy( + entry: AccountProxyConfig | null, + savedProxies: SavedProxy[] +): InlineProxy | null { + if (!entry) return null; + if (entry.proxyId) { + const found = savedProxies.find((p) => p.id === entry.proxyId); + if (!found || !found.host) return null; + return { type: found.type || "socks5", host: found.host, port: Number(found.port) || 0 }; + } + return entry.proxy ?? null; } export default function NoAuthAccountCard({ @@ -60,6 +99,9 @@ export default function NoAuthAccountCard({ const [loading, setLoading] = useState(true); const [adding, setAdding] = useState(false); const [proxyAccountId, setProxyAccountId] = useState(null); + const [proxyMode, setProxyMode] = useState<"saved" | "custom">("saved"); + const [savedProxies, setSavedProxies] = useState([]); + const [selectedProxyId, setSelectedProxyId] = useState(""); const [proxyType, setProxyType] = useState("socks5"); const [proxyHost, setProxyHost] = useState(""); const [proxyPort, setProxyPort] = useState("1080"); @@ -85,9 +127,22 @@ export default function NoAuthAccountCard({ } }, [providerId]); + const fetchSavedProxies = useCallback(async () => { + try { + const res = await fetch("/api/settings/proxies"); + if (res.ok) { + const data = await res.json(); + setSavedProxies(Array.isArray(data?.items) ? data.items : []); + } + } catch (err) { + console.error("Failed to fetch saved proxies:", err); + } + }, []); + useEffect(() => { void fetchConnections(); - }, [fetchConnections]); + void fetchSavedProxies(); + }, [fetchConnections, fetchSavedProxies]); useEffect(() => { const handleClickOutside = (e: MouseEvent) => { @@ -162,19 +217,27 @@ export default function NoAuthAccountCard({ }; const openProxyConfig = (accountId: string) => { - const existing = getProxyForFingerprint(accountProxies, accountId); - if (existing) { - setProxyType(existing.type); - setProxyHost(existing.host); - setProxyPort(String(existing.port)); - setProxyUsername(existing.username || ""); - setProxyPassword(existing.password || ""); + const existing = getEntryForFingerprint(accountProxies, accountId); + // Reset custom-form fields, then prefill from whichever shape was stored. + setProxyType("socks5"); + setProxyHost(""); + setProxyPort("1080"); + setProxyUsername(""); + setProxyPassword(""); + setSelectedProxyId(""); + if (existing?.proxyId) { + setProxyMode("saved"); + setSelectedProxyId(existing.proxyId); + } else if (existing?.proxy?.host) { + setProxyMode("custom"); + setProxyType(existing.proxy.type); + setProxyHost(existing.proxy.host); + setProxyPort(String(existing.proxy.port)); + setProxyUsername(existing.proxy.username || ""); + setProxyPassword(existing.proxy.password || ""); } else { - setProxyType("socks5"); - setProxyHost(""); - setProxyPort("1080"); - setProxyUsername(""); - setProxyPassword(""); + // New: default to the Proxy Pool dropdown when pool entries exist. + setProxyMode(savedProxies.length > 0 ? "saved" : "custom"); } setProxyAccountId(accountId); }; @@ -183,21 +246,30 @@ export default function NoAuthAccountCard({ if (!conn || !proxyAccountId) return; setSavingProxy(true); try { - const trimmedHost = proxyHost.trim(); - const newProxy: AccountProxyConfig["proxy"] = trimmedHost - ? { - type: proxyType, - host: trimmedHost, - port: Number(proxyPort) || 1080, - ...(proxyUsername.trim() ? { username: proxyUsername.trim() } : {}), - ...(proxyPassword.trim() ? { password: proxyPassword.trim() } : {}), - } - : null; + const others = accountProxies.filter((p) => p.fingerprint !== proxyAccountId); + let newEntry: AccountProxyConfig | null = null; + if (proxyMode === "saved") { + // Store a REFERENCE (by id); server resolves it to a live proxy record. + newEntry = selectedProxyId + ? { fingerprint: proxyAccountId, proxyId: selectedProxyId } + : null; + } else { + const trimmedHost = proxyHost.trim(); + newEntry = trimmedHost + ? { + fingerprint: proxyAccountId, + proxy: { + type: proxyType, + host: trimmedHost, + port: Number(proxyPort) || 1080, + ...(proxyUsername.trim() ? { username: proxyUsername.trim() } : {}), + ...(proxyPassword.trim() ? { password: proxyPassword.trim() } : {}), + }, + } + : null; + } - const existing = accountProxies.filter((p) => p.fingerprint !== proxyAccountId); - const updatedProxies = newProxy - ? [...existing, { fingerprint: proxyAccountId, proxy: newProxy }] - : existing; + const updatedProxies = newEntry ? [...others, newEntry] : others; const res = await fetch(`/api/providers/${conn.id}`, { method: "PUT", @@ -228,19 +300,12 @@ export default function NoAuthAccountCard({ throw new Error("No saved proxies found. Add proxies in Settings → Proxy first."); } - const updatedProxies: AccountProxyConfig[] = allAccountIds.map((fp, i) => { - const proxy = savedProxies[i % savedProxies.length]; - return { - fingerprint: fp, - proxy: { - type: proxy.type || "socks5", - host: proxy.host, - port: proxy.port, - ...(proxy.username ? { username: proxy.username } : {}), - ...(proxy.password ? { password: proxy.password } : {}), - }, - }; - }); + // #5217 (Gap 1): distribute stores by-id references too, so editing a pool + // proxy later propagates to every account it was distributed to. + const updatedProxies: AccountProxyConfig[] = allAccountIds.map((fp, i) => ({ + fingerprint: fp, + proxyId: savedProxies[i % savedProxies.length].id, + })); const res = await fetch(`/api/providers/${conn.id}`, { method: "PUT", @@ -305,7 +370,7 @@ export default function NoAuthAccountCard({ className="grid max-h-72 grid-cols-1 gap-1.5 overflow-y-auto pr-1 sm:grid-cols-2 lg:grid-cols-3" > {allAccountIds.map((id, i) => { - const proxy = getProxyForFingerprint(accountProxies, id); + const proxy = getDisplayProxy(getEntryForFingerprint(accountProxies, id), savedProxies); return (
-
- setSelectedProxyId(e.target.value)} + className="w-full rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10" + > + + {savedProxies.map((p) => ( + ))} - setProxyHost(e.target.value)} - placeholder="Host" - className="flex-1 rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10" - /> - setProxyPort(e.target.value)} - placeholder="Port" - className="w-16 rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10" - /> -
- setProxyUsername(e.target.value)} - placeholder="Username (optional)" - className="w-full rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10" - /> - setProxyPassword(e.target.value)} - placeholder="Password (optional)" - className="w-full rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10" - /> + ) : ( + <> +
+ + setProxyHost(e.target.value)} + placeholder="Host" + className="flex-1 rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10" + /> + setProxyPort(e.target.value)} + placeholder="Port" + className="w-16 rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10" + /> +
+ setProxyUsername(e.target.value)} + placeholder="Username (optional)" + className="w-full rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10" + /> + setProxyPassword(e.target.value)} + placeholder="Password (optional)" + className="w-full rounded-md border border-black/10 bg-bg px-2.5 py-1.5 text-xs dark:border-white/10" + /> + + )}